diff --git a/.github/workflows/build-ffmpeg.yml b/.github/workflows/build-ffmpeg.yml index a5714369..ab688392 100644 --- a/.github/workflows/build-ffmpeg.yml +++ b/.github/workflows/build-ffmpeg.yml @@ -103,6 +103,7 @@ jobs: install: >- base-devel openssl-devel + vim ${{ matrix.msys_prefix }}-pkgconf ${{ matrix.msys_system == 'CLANGARM64' && format('{0}-clang', matrix.msys_prefix) || format('{0}-gcc {0}-nasm', matrix.msys_prefix) }} msystem: ${{ matrix.msys_system }} @@ -110,6 +111,7 @@ jobs: - name: Build FFmpeg env: CIBW_ARCHS: ${{ matrix.msys_system == 'CLANGARM64' && 'ARM64' || (matrix.msys_prefix && 'AMD64' || matrix.arch) }} + CIBW_BEFORE_ALL_LINUX: python scripts/install-vmaf-build-tools.py CIBW_BEFORE_BUILD: python scripts/build-ffmpeg.py /tmp/vendor CIBW_BEFORE_BUILD_WINDOWS: python scripts\build-ffmpeg.py C:\cibw\vendor CIBW_BUILD: cp311-* @@ -125,6 +127,7 @@ jobs: with: name: output-${{ matrix.os }} path: output/ + if-no-files-found: error cross-build: needs: fan @@ -151,7 +154,7 @@ jobs: - name: Build FFmpeg env: CIBW_ARCHS: ${{ matrix.arch }} - CIBW_BEFORE_ALL_LINUX: ./scripts/install-static-clang.sh + CIBW_BEFORE_ALL_LINUX: python scripts/install-vmaf-build-tools.py && ./scripts/install-static-clang.sh CIBW_BEFORE_BUILD_LINUX: python scripts/build-ffmpeg.py /tmp/vendor CIBW_BUILD: cp311-${{ matrix.build }}${{ matrix.arch }} CIBW_ENVIRONMENT_LINUX: > @@ -171,6 +174,7 @@ jobs: with: name: output-${{ matrix.build }}${{ matrix.arch }} path: output/ + if-no-files-found: error upload-release: if: github.event_name == 'release' diff --git a/README.md b/README.md index 6853108a..5fd59d59 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Currently FFmpeg 8.1.2 is built with the following packages enabled for all plat - png 1.6.58 - webp 1.6.0 - opencore-amr 0.1.6 +- vmaf 3.2.0 - x264 32c3b801191522961102d4bea292cdb61068d0dd (except armv7l) - x265 4.2 (except armv7l) diff --git a/scripts/build-ffmpeg.py b/scripts/build-ffmpeg.py index 0c5535a5..0bb344b2 100644 --- a/scripts/build-ffmpeg.py +++ b/scripts/build-ffmpeg.py @@ -82,6 +82,41 @@ def make_tarball_name() -> str: else: return "ffmpeg-unknown" + +def validate_output_tarball(path: str) -> None: + runtime_member = None + with tarfile.open(path, "r:gz") as tar: + for member in tar.getmembers(): + if not member.isfile() or member.size <= 0: + continue + + name = member.name.lower().replace("\\", "/") + basename = os.path.basename(name) + if plat == "Windows": + matches = name == "bin/libvmaf.dll" + elif plat == "Darwin": + matches = ( + name.startswith("lib/") + and basename.startswith("libvmaf.") + and basename.endswith(".dylib") + ) + else: + matches = ( + name.startswith("lib/") + and basename.startswith("libvmaf.so.") + ) + + if matches: + runtime_member = (member.name, member.size) + + if runtime_member is None: + raise RuntimeError(f"VMAF runtime library missing from {path}") + + print( + f"{os.path.basename(path)}: {os.path.getsize(path)} bytes " + f"({runtime_member[0]}: {runtime_member[1]} bytes)" + ) + def main(): parser = argparse.ArgumentParser("build-ffmpeg") parser.add_argument("destination") @@ -112,6 +147,7 @@ def main(): output_tarball = os.path.join(output_dir, make_tarball_name() + ".tar.gz") if os.path.exists(output_tarball): + validate_output_tarball(output_tarball) return builder = Builder(dest_dir=dest_dir) @@ -159,6 +195,7 @@ def main(): "--enable-libopencore-amrwb", "--enable-libopus", "--enable-libsvtav1", + "--enable-libvmaf", "--enable-libvpx", "--enable-libwebp", "--enable-libxcb" if plat == "Linux" else "--disable-libxcb", @@ -342,6 +379,8 @@ def main(): with open(filepath, "rb") as f: tar.addfile(info, f) + validate_output_tarball(output_tarball) + if __name__ == "__main__": main() diff --git a/scripts/install-vmaf-build-tools.py b/scripts/install-vmaf-build-tools.py new file mode 100644 index 00000000..ec4a8133 --- /dev/null +++ b/scripts/install-vmaf-build-tools.py @@ -0,0 +1,55 @@ +import os +import shutil +import subprocess +import tempfile + + +def xxd_supports_model_embedding() -> bool: + xxd = shutil.which("xxd") + if xxd is None: + return False + + with tempfile.TemporaryDirectory() as temp_dir: + input_path = os.path.join(temp_dir, "input") + output_path = os.path.join(temp_dir, "output.c") + with open(input_path, "wb") as input_file: + input_file.write(b"vmaf") + result = subprocess.run( + [xxd, "-i", input_path, output_path], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + return ( + result.returncode == 0 + and os.path.exists(output_path) + and os.path.getsize(output_path) > 0 + ) + + +def main() -> None: + if xxd_supports_model_embedding(): + return + + if shutil.which("apt-get"): + subprocess.run(["apt-get", "update"], check=True) + subprocess.run(["apt-get", "install", "-y", "xxd"], check=True) + else: + package_commands = ( + ("apk", ["apk", "add", "--no-cache", "xxd"]), + ("dnf", ["dnf", "install", "-y", "vim-common"]), + ("yum", ["yum", "install", "-y", "vim-common"]), + ) + for package_manager, command in package_commands: + if shutil.which(package_manager): + subprocess.run(command, check=True) + break + else: + raise RuntimeError("Unable to install xxd for VMAF model embedding") + + if not xxd_supports_model_embedding(): + raise RuntimeError("xxd does not support VMAF model embedding") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/pkg.py b/scripts/pkg.py index 957dc558..0120e107 100644 --- a/scripts/pkg.py +++ b/scripts/pkg.py @@ -131,6 +131,21 @@ def __lt__(self, other): sha256="483eb4061088e2b34b358e47540b5d495a96cd468e361050fae615b1809dc4a1", build_arguments=["--disable-dependency-tracking"], ), + Package( + name="vmaf", + source_url="https://github.com/Netflix/vmaf/archive/refs/tags/v3.2.0.tar.gz", + source_filename="vmaf-3.2.0.tar.gz", + sha256="a28f93f3b4fa65601be324587072e32a6a704a304ba7b1aec9b70b3f709bc1dc", + build_system="meson", + source_dir="libvmaf", + build_arguments=[ + "--buildtype=release", + "-Ddefault_library=shared", + "-Denable_tests=false", + "-Denable_docs=false", + "-Denable_tools=false", + ], + ), Package( name="x264", source_url="https://code.videolan.org/videolan/x264/-/archive/b35605ace3ddf7c1a5d67a2eb553f034aef41d55/x264-b35605ace3ddf7c1a5d67a2eb553f034aef41d55.tar.bz2", @@ -199,7 +214,7 @@ def __lt__(self, other): all_packages: list[Package] = [ffmpeg_package] all_packages.extend(codec_group) -all_packages.extend(gnutls_group) +all_packages.extend(gnutls_group) all_packages.extend( [nasm_package, alsa_package, nvheaders_package, amfheaders_package, libvpl_package] ) diff --git a/src/dummy/binding.c b/src/dummy/binding.c index bfb2dee3..2fb10fb5 100644 --- a/src/dummy/binding.c +++ b/src/dummy/binding.c @@ -2,17 +2,53 @@ #include #include "libavcodec/avcodec.h" #include "libavdevice/avdevice.h" +#include "libavfilter/avfilter.h" #include "libavformat/avformat.h" #define MODULE_NAME "dummy.binding" +static int +verify_libvmaf_filter(void) +{ + const AVFilter *libvmaf; + AVFilterContext *libvmaf_context; + AVFilterGraph *graph; + + libvmaf = avfilter_get_by_name("libvmaf"); + if (libvmaf == NULL) { + PyErr_SetString(PyExc_RuntimeError, "FFmpeg libvmaf filter is unavailable"); + return -1; + } + + graph = avfilter_graph_alloc(); + if (graph == NULL) { + PyErr_NoMemory(); + return -1; + } + if (avfilter_graph_create_filter( + &libvmaf_context, libvmaf, "libvmaf", NULL, NULL, graph) < 0) { + avfilter_graph_free(&graph); + PyErr_SetString( + PyExc_RuntimeError, + "FFmpeg libvmaf filter initialization failed" + ); + return -1; + } + avfilter_graph_free(&graph); + + return 0; +} + static PyObject* -test(PyObject *args) +test(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(ignored)) { // initialise libraries avformat_network_init(); avdevice_register_all(); + if (verify_libvmaf_filter() < 0) + return NULL; + fprintf(stderr, "FFmpeg is OK\n"); Py_RETURN_NONE;