В свое время при изучении вопроса кодирования видео меня зацепила фраза из руководств по FFmpeg, что библиотека fdk-aac обеспечивает лучшее качество звука по сравнению с нативными реализациями aac и ac3. К сожалению, в соответствии с лицензией, ее нельзя включать в сборку для распространения. Однако ничто не мешает скомпилировать свой ffmpeg с libfdk_aac и libvvenc.

Введение

Наверное надо внести ясность, о чем вообще идет речь. FFmpeg, как они сами о себе пишут – передовой мультимедийный фреймворк для (де)кодирования, (де)мультиплексирования, стриминга, фильтрации и воспроизведения практически всего подряд. В более узком смысле (и нижнем регистре) – утилита командной строки. Недостатков у последней примерно два: не слишком очевидные параметры (причем зачастую важен порядок их следования) и немалый размер полных сборок (скажем версия 9.0.2 от gyan.dev весит 217 мегабайт). А, ну еще с .mkv лучше работать с помощью MKVToolNix (т.е. с помощью ffmpeg перекодировать только то, что надо, а потом отдельно пересобрать).

AAC – формат сжатия звука с потерями, «родной» для контейнеров .mp4 и вообще один из наиболее широко поддерживаемых аудиоформатов, в том числе в веб. AC3 (тоже с потерями) – стандарт для носителей (DVD, Blu-Ray). Вообще-то стоит еще упомянуть Opus: чуть ли не вообще самый лучший (среди форматов с потерями), но за пределами интернетов как будто поддерживается не столь широко, как первые два.

libvvenc просто пришлась к слову, это вроде как эталонная реализация кодировщика в новейший формат видео H.266 (VVC) и которую мы помимо прочего включим в нашу сборку. Кстати она (эта библиотека) не обязательно входит в состав ffmpeg, например ее нет во FreeBSD.

Подготовка

FFmpeg поддерживает множество различных форматов, поэтому для начала определимся, а что вообще должна включать в себя сборка. Раз мы упомянули веб-форматы, то они предусматривают сочетание H.264 + AAC (.mp4) или AV1 + Opus (.webm). Разумеется, без H.265 картина была бы неполной. Также предлагаю подключить несколько вспомогательных библиотек. Получаем:

  • Видео:
    • x264 – кодирование в формат H.264 (AVC)
    • x265 – H.265 (HEVC)
    • VVenC – H.266 (VVC)
    • SVT-AV1 – реализация кодировщика в AV1, с которой хоть как-то можно работать (но не нужно, судя по выдаваемому результату)
    • dav1d – «эталонный» декодер формата AV1
    • VMAF – некая метрика качества кодирования
  • Аудио:
    • fdk-aac – ради чего все и затевалось в немалой степени
    • Opus
  • Изображения:
    • zimg – позволяет, помимо прочего, преобразовать HDR в SDR
    • zlib – поддержка сжатия; она необходима, в частности, для работы с PNG
  • Разное:
    • libbluray – поддержка Blu-Ray дисков (разумеется, незащищенных)

Честно говоря, я сомневался, подключать ли libbluray, когда есть MakeMKV или т.п. Но в целом это оказалось несложно, в отличие от DVD (поддержка которых требует сразу двух библиотек, да еще и вроде бы не самым очевидным образом компилирующихся).

Существуют официальные руководства по компиляции, на которые я в немалой степени и ориентировался:

А еще помог анализ исходников сборки от BtbN.

Чтобы не засорять основную систему, предлагаю работать в Docker'е. Хост в моем случае – виртуалка с Arch Linux, которая в свою очередь крутится под Windows. wacko Будем считать, что текущий пользователь имеет доступ к «розетке» или, иными словами, включен в группу docker, чтобы не заморачиваться еще и с sudo.

В общем-то поэтому и кросс-компиляция: находясь в Linux, мы будем компилировать программу для Windows. Ну и для самой Linux тоже заодно.

Начнем формировать Dockerfile. Будем использовать многоэтапную сборку, чтобы не засорять историю конечного образа. Я предпочитаю Debian, поэтому в качестве базового образа возьмем актуальный на момент написания trixie-slim. Справедливости ради, в Убунте возможно был бы более «свежий» компилятор, но не суть. Установим вышеупомянутый компилятор (MinGW и, как следствие, gcc) и всякие прочие средства сборки:

FROM debian:trixie-slim AS builder

# Install build tools and some dependencies
RUN set -eux; \
        apt-get update; \
        apt-get install -y --no-install-recommends \
            autoconf \
            automake \
            build-essential \
            ca-certificates \
            cmake \
            g++-mingw-w64-x86-64 \
            gcc-mingw-w64-x86-64 \
            git \
            libtool \
            meson \
            nasm \
            ninja-build \
            pkg-config \
            wget \
            xxd \
        ; \
        apt-get dist-clean

Заодно обновим сертификаты и установим git с wget'ом. Помимо стандартного make (GNU make, если угодно), часть библиотек собираются cmake, а еще часть – Ниндзей с Мезоном. В качестве ассемблера я выбрал якобы более актуальный NASM. Скорее всего с точки зрения компиляции x265 и SVT-AV1 других вариантов и нет.

Чтобы не делать два разных Dockerfile, специфические опции компиляции будем задавать через аргументы и переменные окружения:

ARG BUILD_ARCH=x86_64 \
    BUILD_TARGET_OS=mingw32 \
    BUILD_HOST=x86_64-w64-mingw32 \
    BUILD_CROSS_PREFIX=${BUILD_HOST}- \
    BUILD_CMAKE_TOOLCHAIN=windows.cmake \
    BUILD_MESON_TOOLCHAIN=windows.meson \
    AR="${BUILD_CROSS_PREFIX}gcc-ar" \
    CFLAGS="-static -O2 -pipe -D_FORTIFY_SOURCE=2 -fstack-protector-strong" \
    CXXFLAGS="-static -O2 -pipe -D_FORTIFY_SOURCE=2 -fstack-protector-strong" \
    LDFLAGS="-static -O2 -pipe -fstack-protector-strong"

ENV AR=$AR \
    PKG_CONFIG=pkg-config \
    CFLAGS=$CFLAGS \
    CXXFLAGS=$CXXFLAGS \
    LDFLAGS=$LDFLAGS

COPY $BUILD_CMAKE_TOOLCHAIN $BUILD_MESON_TOOLCHAIN /usr/src/

Значения по умолчанию характерны для компиляции для Windows, более же родные мы в дальнейшем переопределим с помощью compose.yaml.

Будем делать так называемую статическую сборку, чтобы не возиться со всякого рода библиотеками. Справедливости ради, для Linux сборка получится не совсем статической, так как останется зависимость от некоторых системных библиотек (в частности стандартных библиотек C и C++). Из-за этого программа может не работать, например, в Rocky Linux 8.

Директивой COPY помещаем в образ настройки cmake и meson для соответствующих операционных систем.

windows.cmake:

#!cmake

# https://github.com/Multicorewareinc/x265/wiki/CrossCompile
# https://github.com/BtbN/FFmpeg-Builds/blob/master/images/base-win64/toolchain.cmake

# this one is important
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)

set(triple x86_64-w64-mingw32)

# specify the cross compiler
set(CMAKE_C_COMPILER ${triple}-gcc)
set(CMAKE_CXX_COMPILER ${triple}-g++)
set(CMAKE_RC_COMPILER ${triple}-windres)
set(CMAKE_RANLIB ${triple}-gcc-ranlib)
set(CMAKE_AR ${triple}-gcc-ar)

windows.meson:

[binaries]
c = 'x86_64-w64-mingw32-gcc'
cpp = 'x86_64-w64-mingw32-g++'
ar = 'x86_64-w64-mingw32-gcc-ar'
ranlib = 'x86_64-w64-mingw32-gcc-ranlib'
strip = 'x86_64-w64-mingw32-strip'
windres = 'x86_64-w64-mingw32-windres'
dlltool = 'x86_64-w64-mingw32-dlltool'

[host_machine]
system = 'windows'
cpu_family = 'x86_64'
cpu = 'x86_64'
endian = 'little'

linux.cmake:

#!cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)

linux.meson:

[binaries]
c = 'gcc'
cpp = 'g++'
ld = 'ld'
ar = 'ar'
ranlib = 'ranlib'
strip = 'strip'

[host_machine]
system = 'linux'
cpu_family = 'x86_64'
cpu = 'x86_64'
endian = 'little'

Суть в немалой степени в том, что для кросс-компиляции необходимо использовать специальные версии программ (компилятора, линковщика и т.д.) из набора MinGW.

Компиляция

x264

Начнем со старой доброй libx264, которая компилируется через обычный make. К примеру, аналогичным образом мы ранее собирали PHP.

# libx264
ARG X264_VERSION=master
RUN set -eux; \
        cd /usr/src; \
        git clone --depth 1 --branch $X264_VERSION https://code.videolan.org/videolan/x264.git; \
        cd x264; \
        ./configure \
            --host=$BUILD_HOST \
            --cross-prefix=$BUILD_CROSS_PREFIX \
            --disable-cli \
            --enable-static \
            --enable-strip \
            --enable-pic \
            --disable-lavf \
            --disable-swscale \
        ; \
        make -j$(nproc); \
        make install; \
        cd /usr/src; \
        rm -rf x264

В большинстве случаев схема стандартная. Клонируем тег (ветку) git-репозитория без истории (т.е. глубиной 1), соответствующей версии библиотеки, переданной через аргумент сборки образа, в подкаталог /usr/src. Указываем параметры сборки, компилируем, устанавливаем и удаляем исходные коды.

В данном случае (в параметрах ./configure) мы указали хост (пустой для Linux или x86_64-w64-mingw32 для Windows), префикс кросс-компиляции (пустой для Linux или хост, дополненный дефисом, для Windows), отключили сборку утилиты командной строки, а также включили статическую сборку, очистку символов отладки (strip.) и компиляцию позиционно-независимого кода (build position-independent code). По поводу необходимости последнего флага у меня есть некоторые сомнения, но он присутствует как в руководстве от FFmpeg, так и у BtbN.

Еще мы отключили поддержку некоторых библиотек, которые и так есть в самом ffmpeg – --disable-lavf (libavformat) и --disable-swscale. А еще можно ограничиться лишь 8-битным цветом: --bit-depth=8 – 10-битный AVC науке почти неизвестен. Впрочем без 10 бит будет не вполне корректно сравнивать результаты с HEVC и VVC. А еще, как оказалось, будет врать встроенная справка ffmpeg -h encoder=libx264 – показывать неподдерживаемые «10-битные» форматы цвета.

x265

Эту библиотеку, очевидно, писали какие-то гении. В хорошем смысле, потому что она в немалой степени на ассемблере, а в плохом – для полной поддержки всех возможных цветов радуги необходимо по сути скомпилировать библиотеку трижды (для 8, 10 и 12 бит) и затем это все дело объединить. crazy  cmake – вишенка на торте.

# libx265
ARG X265_VERSION=master \
    X265_CMAKE_CONFIG="-DCMAKE_TOOLCHAIN_FILE=/usr/src/${BUILD_CMAKE_TOOLCHAIN} -DCMAKE_BUILD_TYPE=Release -DENABLE_SHARED=OFF -DENABLE_CLI=OFF -DENABLE_ALPHA=ON"
RUN set -eux; \
        cd /usr/src; \
        git clone --depth 1 --branch $X265_VERSION https://github.com/Multicorewareinc/x265.git; \
        cd x265/build/linux; \
        # build custom multilib (see multilib.sh as reference)
        cpu_count=$(nproc); \
        mkdir -p 8bit 10bit 12bit; \
        # build main12
        cd 12bit; \
        cmake ../../../source -Wno-dev \
            $X265_CMAKE_CONFIG \
            -DHIGH_BIT_DEPTH=ON \
            -DEXPORT_C_API=OFF \
            -DMAIN12=ON \
        ; \
        make -j${cpu_count}; \
        # build main10
        cd ../10bit; \
        cmake ../../../source -Wno-dev \
            $X265_CMAKE_CONFIG \
            -DHIGH_BIT_DEPTH=ON \
            -DEXPORT_C_API=OFF \
            -DENABLE_HDR10_PLUS=ON \
        ; \
        make -j${cpu_count}; \
        # build main (8 bit) with linked 10 and 12 bit
        cd ../8bit; \
        ln -sf ../10bit/libx265.a libx265_main10.a; \
        ln -sf ../12bit/libx265.a libx265_main12.a; \
        cmake ../../../source -Wno-dev \
            $X265_CMAKE_CONFIG \
            -DEXTRA_LIB="x265_main10.a;x265_main12.a" \
            -DEXTRA_LINK_FLAGS=-L. \
            -DLINKED_10BIT=ON \
            -DLINKED_12BIT=ON \
        ; \
        make -j${cpu_count}; \
        # rename the 8bit library, then combine all three into libx265.a
        mv libx265.a libx265_main.a; \
        # On Linux, we use GNU ar to combine the static libraries together
        { \
            echo 'CREATE libx265.a'; \
            echo 'ADDLIB libx265_main.a'; \
            echo 'ADDLIB libx265_main10.a'; \
            echo 'ADDLIB libx265_main12.a'; \
            echo 'SAVE'; \
            echo 'END'; \
        } | ${AR} -M -; \
        make install; \
        cd /usr/src; \
        rm -rf x265

Небольшая историческая справка. Еще одно проявление «гениальности» заключалось в размещении исходного кода на BitBucket, при этом при клонировании по тегу библиотека то ли криво собиралась, то ли не собиралась вовсе, а в довершении всего стабильная ветка соответствовала древней версии 4.1. К счастью, они все же переехали на GitHub и все эти проблемы починили, в частности актуальная версия теперь 4.3 от июля 2026 года.

Разберем код выше по частям. Общие параметры компиляции я выделил в аргумент Докера. Они определяют сборку релизной версии статической библиотеки, без утилиты командной строки, с поддержкой альфа-канала (если я правильно понял назначение -DENABLE_ALPHA=ON – enable alpha encoding), с использованием заданного набора программ (linux.cmake или windows.cmake). Параметры cmake в явном виде как будто не задокументированы, но есть некий CMakeLists.txt, исходя из которого можно что-то приблизительно понять (не будь мы в Докере, можно было бы запустить cmake -LAH).

Клонируем репозиторий, переходим в каталог сборки для Linux, между делом запоминаем количество процессоров и создаем три подкаталога для соответствующих версий библиотек.

Компилируем версию для 12 бит:

        # build main12
        cd 12bit; \
        cmake ../../../source -Wno-dev \
            $X265_CMAKE_CONFIG \
            -DHIGH_BIT_DEPTH=ON \
            -DEXPORT_C_API=OFF \
            -DMAIN12=ON \
        ; \
        make -j${cpu_count}; \

Флагом -Wno-dev подавляем вывод предупреждений для разработчиков (их и без этого хватает), включаем 12 бит (-DHIGH_BIT_DEPTH=ON -DMAIN12=ON) и отключаем экспорт API (-DEXPORT_C_API=OFF). Собственно этим самым экспортом будет заведовать 8-битная версия, к которой будут привязаны остальные.

Аналогичным образом компилируем 10 бит (с поддержкой HDR10+):

        # build main10
        cd ../10bit; \
        cmake ../../../source -Wno-dev \
            $X265_CMAKE_CONFIG \
            -DHIGH_BIT_DEPTH=ON \
            -DEXPORT_C_API=OFF \
            -DENABLE_HDR10_PLUS=ON \
        ; \
        make -j${cpu_count}; \

Переходим к сборке библиотеки для 8-битного цвета. Создадим симлинки на ранее сформированные библиотеки и подключим их к основной версии:

        # build main (8 bit) with linked 10 and 12 bit
        cd ../8bit; \
        ln -sf ../10bit/libx265.a libx265_main10.a; \
        ln -sf ../12bit/libx265.a libx265_main12.a; \
        cmake ../../../source -Wno-dev \
            $X265_CMAKE_CONFIG \
            -DEXTRA_LIB="x265_main10.a;x265_main12.a" \
            -DEXTRA_LINK_FLAGS=-L. \
            -DLINKED_10BIT=ON \
            -DLINKED_12BIT=ON \
        ; \
        make -j${cpu_count}; \

На выходе получили очередную статическую библиотеку libx265.a. Переименуем её и с помощью утилиты ar соберем все воедино:

        # rename the 8bit library, then combine all three into libx265.a
        mv libx265.a libx265_main.a; \
        # On Linux, we use GNU ar to combine the static libraries together
        { \
            echo 'CREATE libx265.a'; \
            echo 'ADDLIB libx265_main.a'; \
            echo 'ADDLIB libx265_main10.a'; \
            echo 'ADDLIB libx265_main12.a'; \
            echo 'SAVE'; \
            echo 'END'; \
        } | ${AR} -M -; \

Уфф tired, наконец-то можно установить полученного монстра и удалить все нафиг:

        make install; \
        cd /usr/src; \
        rm -rf x265

Но открою небольшой секрет: если нужен только 10-битный цвет (а HEVC как раз в основном об этом), то можно обойтись одной:

# libx265
ARG X265_VERSION=master
RUN set -eux; \
        cd /usr/src; \
        git clone --depth 1 --branch $X265_VERSION https://github.com/Multicorewareinc/x265.git; \
        cd x265/build/linux; \
        cmake ../../source -Wno-dev \
            -DCMAKE_TOOLCHAIN_FILE=/usr/src/${BUILD_CMAKE_TOOLCHAIN} \
            -DCMAKE_BUILD_TYPE=Release \
            -DENABLE_SHARED=OFF \
            -DENABLE_CLI=OFF \
            -DENABLE_ALPHA=ON \
            -DHIGH_BIT_DEPTH=ON \
            -DENABLE_HDR10_PLUS=ON \
        ; \
        make -j$(nproc); \
        make install; \
        cd /usr/src; \
        rm -rf x265

Как и в случае с x264, встроенная справка будет показывать недоступные «8-битные» цвета, а при попытке кодирования в таком формате будет выполнено «неявное» преобразование к аналогичному варианту для 10 бит. Но если выигрыша от отключения 10 бит для x264 особого нет, то в данном случае время компиляции заметно уменьшается, как и общий размер ffmpeg'а, потому что каждый вариант x265 занимает несколько мегабайт.

dav1d

Декодер AV1 собирается с помощью meson и ninja, но в остальном все аналогично – компилируем релизную статическую библиотеку:

# libdav1d
ARG DAV1D_VERSION=master
RUN set -eux; \
        cd /usr/src; \
        git clone --depth 1 --branch $DAV1D_VERSION https://code.videolan.org/videolan/dav1d.git; \
        mkdir -p dav1d/build; \
        cd dav1d/build; \
        meson setup .. \
            -Denable_tools=false \
            -Denable_tests=false \
            --buildtype=release \
            --cross-file=/usr/src/$BUILD_MESON_TOOLCHAIN \
            --default-library=static \
        ; \
        ninja -j$(nproc); \
        ninja install; \
        cd /usr/src; \
        rm -rf dav1d

SVT-AV1

После x265 cmake'ом никого не удивишь:

# libsvtav1
ARG SVTAV1_VERSION=master
RUN set -eux; \
        cd /usr/src; \
        git clone --depth 1 --branch $SVTAV1_VERSION https://gitlab.com/AOMediaCodec/SVT-AV1.git; \
        cd SVT-AV1/Build; \
        cmake .. \
            -DCMAKE_TOOLCHAIN_FILE=/usr/src/${BUILD_CMAKE_TOOLCHAIN} \
            -DCMAKE_BUILD_TYPE=Release \
            -DBUILD_APPS=OFF \
            -DBUILD_SHARED_LIBS=OFF \
            -DBUILD_TESTING=OFF \
        ; \
        make -j$(nproc); \
        make install; \
        cd /usr/src; \
        rm -rf SVT-AV1

Отключил сборку тестов, разделяемых библиотек и приложений. В официальной инструкции предлагалось отключить декодер: -DBUILD_DEC=OFF, но в актуальных версиях такого флага уже нет (и был ли вообще?). Есть некая «минимальная сборка», но, как я понял, это больше про опции компилятора.

VVenC

Еще один «клиент» cmake. Здесь он осуществляет не только настройку, но и саму компиляцию и установку:

# libvvenc
ARG VVENC_VERSION=master
RUN set -eux; \
        cd /usr/src; \
        git clone --depth 1 --branch $VVENC_VERSION https://github.com/fraunhoferhhi/vvenc.git; \
        cd vvenc; \
        cmake -S . -B build/release-static \
            -DCMAKE_TOOLCHAIN_FILE=/usr/src/${BUILD_CMAKE_TOOLCHAIN} \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=Release \
            -DVVENC_LIBRARY_ONLY=ON \
            -DVVENC_ENABLE_LINK_TIME_OPT=OFF \
        ; \
        cmake --build build/release-static -j$(nproc); \
        cmake --build build/release-static --target install; \
        cd /usr/src; \
        rm -rf vvenc

Новинка – явное указание каталога исходного кода -S . (вроде бы не обязательное вообще-то, но пусть будет как в инструкции) и каталога для сборки (-B build/release-static).

VMAF

Очередной пример сочетания meson и ninja. Отключил формирование документации и тестов, но включил встроенные модели, поддержку AVX512 и вычислений с плавающей точкой (-Denable_float=true). Возможно их уже и не стоит включать, но флаг используется в сборке от BtbN.

# libvmaf
ARG VMAF_VERSION=master
RUN set -eux; \
        cd /usr/src; \
        git clone --depth 1 --branch $VMAF_VERSION https://github.com/Netflix/vmaf.git; \
        mkdir -p vmaf/libvmaf/build; \
        cd vmaf/libvmaf/build; \
        meson setup .. \
            -Dbuilt_in_models=true \
            -Denable_avx512=true \
            -Denable_docs=false \
            -Denable_float=true \
            -Denable_tests=false \
            --buildtype=release \
            --cross-file=/usr/src/$BUILD_MESON_TOOLCHAIN \
            --default-library=static \
        ; \
        ninja -j$(nproc); \
        ninja install; \
        cd /usr/src; \
        rm -rf vmaf

fdk-aac

Плавно переходим к звуку. У этой библиотеки из особенностей компиляции разве что вызов autoreconf, а в остальном – стандартный ./configure && make.

# libfdk_aac
ARG FDK_AAC_VERSION=master
RUN set -eux; \
        cd /usr/src; \
        git clone --depth 1 --branch $FDK_AAC_VERSION https://github.com/mstorsjo/fdk-aac.git; \
        cd fdk-aac; \
        autoreconf -fiv; \
        ./configure \
            --host=$BUILD_HOST \
            --disable-example \
            --disable-shared \
            --enable-static \
            --with-pic \
        ; \
        make -j$(nproc); \
        make install; \
        cd /usr/src; \
        rm -rf fdk-aac

Opus

Здесь лично у меня возникли трудности с загрузкой некоей модели машинного обучения, но без которой вроде и так все замечательно собирается. Впрочем, с этой точки зрения, возможно лучше было бы предустановить пакет libopus-dev, а не заниматься компиляцией.

ARG OPUS_VERSION=main
RUN set -eux; \
        cd /usr/src; \
        git clone --depth 1 --branch $OPUS_VERSION https://github.com/xiph/opus.git; \
        cd opus; \
        autoreconf -isf; \
        ./configure \
            --host=$BUILD_HOST \
            --disable-doc \
            --disable-extra-programs \
            --disable-shared \
        ; \
        make -j$(nproc); \
        make install; \
        cd /usr/src; \
        rm -rf opus

zimg

Переходим к работе с изображением. Из интересного здесь обновление субмодулей git:

# zimg
ARG ZIMG_VERSION=master
RUN set -eux; \
        cd /usr/src; \
        git clone --depth 1 --branch $ZIMG_VERSION https://github.com/sekrit-twc/zimg.git; \
        cd zimg; \
        git submodule update --init --recursive --depth 1; \
        ./autogen.sh; \
        ./configure \
            --host=$BUILD_HOST \
            --disable-shared \
            --enable-static \
            --with-pic \
        ; \
        make -j$(nproc); \
        make install; \
        cd /usr/src; \
        rm -rf zimg

Несмотря на то, что эта библиотека помимо прочего может изменять размер изображения (zscale), на мой взгляд стандартный ffmpeg'овский фильтр scale работает намного лучше (кинематографичнее, что ли). Тем не менее, вот как можно преобразовать изображение (видео) с расширенным динамическим диапазоном в стандартный (значение параметра -vf, он же -filter:v):

zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p

Рецепт был когда-то найден на просторах интернета, но, к сожалению, страницы с объяснением больше не существует. Один момент: npl=100 возможно дает темноватое изображение и стоит ставить ближе к 50. Кстати вопрос – в какой момент менять разрешение, ведь результат от перемены мест меняется. Предположительно перед окончательным изменением формата, т.е. почти в самом конце.

zlib

В данном случае оказалось проще скачать архив с исходным кодом, нежели что-то откуда-то клонировать. Вот и wget пригодился. Перед вызовом ./configure потребовалось определить переменную окружения CHOST, а в остальном все как обычно.

# zlib (PNG etc.)
RUN set -eux; \
        cd /usr/src; \
        wget https://zlib.net/current/zlib.tar.gz; \
        tar xf zlib.tar.gz; \
        rm zlib.tar.gz; \
        cd zlib*; \
        CHOST=${BUILD_HOST} ./configure \
            --static \
        ; \
        make -j$(nproc); \
        make install; \
        cd /usr/src; \
        rm -rf zlib*

Libbluray

Эту библиотеку я скомпилировал в самом упрощенном виде, потому что иначе пришлось бы тащить еще кучу зависимостей (их наличие определяется автоматически на этапе meson setup).

# libbluray
ARG BLURAY_VERSION=master
RUN set -eux; \
        cd /usr/src; \
        git clone --depth 1 --branch $BLURAY_VERSION https://code.videolan.org/videolan/libbluray.git; \
        mkdir libbluray/build; \
        cd libbluray/build; \
        export CPPFLAGS="${CXXFLAGS} -Ddec_init=libbr_dec_init"; \
        meson setup .. \
            -Ddefault_library=static \
            -Denable_docs=false \
            -Denable_tools=false \
            -Denable_devtools=false \
            -Denable_examples=false \
            -Dbdj_jar=disabled \
            --cross-file=/usr/src/$BUILD_MESON_TOOLCHAIN \
        ; \
        ninja -j$(nproc); \
        ninja install; \
        cd /usr/src; \
        rm -rf libbluray

ffmpeg

Наконец-то все «внешние» библиотеки готовы и можно приступать к сборке самого ffmpeg'а. Я предпочел скачивать архив с сайта.

ARG FFMPEG_VERSION=snapshot
RUN set -eux; \
        cd /usr/src; \
        wget -O ffmpeg.tar.bz2 https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2; \
        tar xf ffmpeg.tar.bz2; \
        rm ffmpeg.tar.bz2; \
        cd ffmpeg*; \
        ./configure \
            --arch=$BUILD_ARCH \
            --target-os=$BUILD_TARGET_OS \
            --cross-prefix=$BUILD_CROSS_PREFIX \
            --ld="${BUILD_CROSS_PREFIX}g++" \
            --pkg-config=pkg-config \
            --pkg-config-flags="--static" \
            --extra-version=libfdk-aac \
            --enable-gpl \
            --enable-nonfree \
            --disable-debug \
            --disable-doc \
            --disable-ffplay \
            --disable-ffprobe \
            --disable-network \
            --disable-w32threads \
            --enable-libbluray \
            --enable-libdav1d \
            --enable-libfdk-aac \
            --enable-libopus \
            --enable-libsvtav1 \
            --enable-libvmaf \
            --enable-libvvenc \
            --enable-libx264 \
            --enable-libx265 \
            --enable-libzimg \
        || tail -n 30 ffbuild/config.log; \
        make -j$(nproc); \
        make install; \
        cd /usr/src; \
        rm -rf ffmpeg*

Здесь довольно много опций компиляции, которые хотелось бы разобрать.

  • --pkg-config=pkg-config – потребовалось явно прописать, иначе возникали проблемы с поиском библиотек;
  • --extra-version=libfdk-aac – добавил для красоты суффикс версии (отображается при запуске);
  • --enable-gpl --enable-nonfree – управление лицензией, для большинства библиотек требуется gpl, а для libfdk_aac – nonfree;
  • --disable-debug – отключение сборки отладочных версий программ;
  • --disable-doc – отключение установки документации;
  • --disable-ffplay – отключение компиляции программы ffplay. Дело даже не в том, что лично мне (и, надеюсь, вам тоже) она не нужна, а еще и в куче зависимостей от x11 для ее сборки (а как это тащить в Windows и вовсе непонятно);
  • --disable-ffprobe – это уже моя прихоть, так как мне удобнее для этих же целей использовать MediaInfo. Кстати кое-что можно посмотреть и просто через ffmpeg -i без указания выходного файла;
  • --disable-network – оказывается, ffmpeg мог бы забирать файлы прямо из интернета (и нет, это не про трубки), но для поддержки https пришлось бы тащить еще и какую-нибудь GnuTLS… В общем я решил бессовестным образом отключить эту функцию как невостребованную (мной);
  • --disable-w32threads – явно отключил некую библиотеку (потоки Win32), которой в Windows уже нет, а в Linux отродясь не было.

Флаги --enable подключают ранее скомпилированные библиотеки.

Трюк с || tail -n 30 ffbuild/config.log заключается в том, что в случае каких-то проблем на стадии конфигурирования будет сразу же выведена информация об ошибках. В частности так я отловил проблему с pkg-config.

Все, дальше как обычно компилируем и устанавливаем ffmpeg и делаем очистку. Ура! yahoo

Последние штрихи

Нам осталось скопировать полученный результат в «чистый» образ и подготовить его для запуска, чтобы в дальнейшем скопировать ffmpeg из контейнера на хост.

# end-user stage
FROM debian:trixie-slim

COPY --from=builder --chmod=755 /usr/local/bin/ff* /usr/local/bin/
# smoke test
RUN [ ! -x /usr/local/bin/ffmpeg ] || ffmpeg -version

COPY --chmod=755 docker-ffmpeg-entrypoint /usr/local/bin/

ENTRYPOINT ["docker-ffmpeg-entrypoint"]
CMD ["--help"]

«Дымовой тест» показывает версию ffmpeg, но только в варианте для Linux. Для Windows, во-первых, файл называется ffmpeg.exe, а во-вторых по понятным причинам он не запустится.

Еще я написал (слизал у PHP pardon) небольшой входной скрипт для контейнера, файл docker-ffmpeg-entrypoint:

#!/bin/sh
set -e

# first arg is `-f` or `--some-option`
if [ "${1#-}" != "$1" ] && [ -x /usr/local/bin/ffmpeg ]; then
        set -- ffmpeg "$@"
fi

exec "$@"

В сочетании с директивой CMD (в Dockerfile) при запуске контейнера (docker run, если вдруг) по умолчанию будет выведена краткая справка по ffmpeg.

Я предпочитаю docker compose, тем более в данном случае для сборки потребовалось бы передавать множество аргументов. Файл compose.yaml:

services:
  linux:
    build:
      context: .
      args:
        - BUILD_TARGET_OS=linux
        - BUILD_HOST=
        - BUILD_CROSS_PREFIX=
        - BUILD_CMAKE_TOOLCHAIN=linux.cmake
        - BUILD_MESON_TOOLCHAIN=linux.meson
        - AR=ar
        # https://github.com/BtbN/FFmpeg-Builds/blob/master/images/base-linux64/Dockerfile#L67
        - CFLAGS=-static-libgcc -static-libstdc++ -O2 -pipe -fPIC -DPIC -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -pthread
        - CXXFLAGS=-static-libgcc -static-libstdc++ -O2 -pipe -fPIC -DPIC -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -pthread
        - LDFLAGS=-static-libgcc -static-libstdc++ -O2 -pipe -fstack-protector-strong -fstack-clash-protection -Wl,-z,relro,-z,now -pthread -lm
        # versions
        - FFMPEG_VERSION=${FFMPEG_VERSION:-snapshot}
        - X264_VERSION=${X264_VERSION:-master}
        - X265_VERSION=${X265_VERSION:-master}
        - DAV1D_VERSION=${DAV1D_VERSION:-master}
        - SVTAV1_VERSION=${SVTAV1_VERSION:-master}
        - VVENC_VERSION=${VVENC_VERSION:-master}
        - VMAF_VERSION=${VMAF_VERSION:-master}
        - FDK_AAC_VERSION=${FDK_AAC_VERSION:-master}
        - OPUS_VERSION=${OPUS_VERSION:-main}
        - ZIMG_VERSION=${ZIMG_VERSION:-master}
        - BLURAY_VERSION=${BLURAY_VERSION:-master}
    container_name: ffmpeg
    command: "sleep infinity"
    stop_signal: SIGKILL
  windows:
    build:
      context: .
      args:
        - FFMPEG_VERSION=${FFMPEG_VERSION:-snapshot}
        - X264_VERSION=${X264_VERSION:-master}
        - X265_VERSION=${X265_VERSION:-master}
        - DAV1D_VERSION=${DAV1D_VERSION:-master}
        - SVTAV1_VERSION=${SVTAV1_VERSION:-master}
        - VVENC_VERSION=${VVENC_VERSION:-master}
        - VMAF_VERSION=${VMAF_VERSION:-master}
        - FDK_AAC_VERSION=${FDK_AAC_VERSION:-master}
        - OPUS_VERSION=${OPUS_VERSION:-main}
        - ZIMG_VERSION=${ZIMG_VERSION:-master}
        - BLURAY_VERSION=${BLURAY_VERSION:-master}
    container_name: ffmpeg_windows
    command: "sleep infinity"
    stop_signal: SIGKILL

Как видите, в сервисе linux мы как раз переопределили настройки компиляции (BUILD_TARGET_OS и т.д.). Флаги компиляторов и линковщика я бессовестным образом позаимствовал у BtbN, но, как я уже упоминал выше, они как-то не особо помогают для получения полностью статической сборки и требуют в итоге более-менее совместимого Linux'а. Т.е. например в Arch Linux собранный ffmpeg работает, а в Rocky Linux 8 – нет. Возможно нужно как и в случае для Windows просто написать -static, но, если честно, проверять немного не хочется.

Во избежание разночтений явно укажем имена контейнеров, а в качестве команды определим бесконечный сон – нужно, чтобы контейнер работал, пока идет копирование.

Укажем версии библиотек и самого FFmpeg в файле .env. На момент подготовки статьи это были:

FFMPEG_VERSION=9.0.2
X264_VERSION=stable
X265_VERSION=4.3
DAV1D_VERSION=1.5.4
SVTAV1_VERSION=v4.2.0
VVENC_VERSION=release
VMAF_VERSION=v3.2.1
FDK_AAC_VERSION=v2.0.3
OPUS_VERSION=v1.6.1
ZIMG_VERSION=release-3.0.6
BLURAY_VERSION=1.5.1

x264 и VVenC поддерживают универсальные ветки с текущей стабильной (релизной) версией, в остальных случаях приходится указывать их явно.

Наконец, напишем скрипт сборки build:

#!/bin/sh
set -ex

docker compose --progress=plain build linux
docker compose --progress=plain build windows
docker compose up -d

[ -d bin ] || mkdir bin
docker cp ffmpeg:/usr/local/bin/ffmpeg  ./bin/
docker cp ffmpeg_windows:/usr/local/bin/ffmpeg.exe ./bin/

docker compose down

# smoke test
./bin/ffmpeg -version

Выполняем сборку двух образов (для Linux и Windows) с «плоским» выводом логов – в «модном» выводе по умолчанию понять что-либо было бы невозможно, особенно в случае возникновения ошибки конфигурации FFmpeg. Далее «поднимаем» наше «приложение» и копируем исполняемые файлы из контейнеров в подкаталог bin (он, при необходимости, создается). «Вниз», и еще раз проверяем возможность запуска, только теперь уже на хосте.

Не забудьте установить права на запуск скрипта (chmod +x build) и вперед:

./build

Если все пойдет, как задумано, то через какое-то время вы получите свежеиспеченный ffmpeg для Linux и Windows. В зависимости от вашего решения по libx265 объем файлов будет примерно по 50-70 мегабайт. По желанию (и возможности) – выполните очистку Docker'а:

docker system prune -a

Еще можно скопировать (переместить) ffmpeg из подкаталога куда-нибудь в /usr/local/bin (предварить sudo при необходимости):

cp bin/ffmpeg /usr/local/bin/

И каким-то образом отправить в Windows. Можно пользоваться!

Примеры

Извлечь фильм из Blu-Ray (Windows, оптический привод F:):

ffmpeg -i bluray:F:\ -map 0 -ignore_unknown -c copy t00.mkv

Без указания конкретного плейлиста (например ffmpeg -playlist 800 -i bluray:/dev/sr0 ... – вариант для Linux) программа извлечет самое длинное видео. Еще раз повторюсь: у диска не должно быть защиты от копирования, да и в целом для подобной задачи лучше пользоваться чем-то еще.

Внимание! Далее идут по сути боевые настройки кодирования видео, благодаря чему фильм может обрабатываться вплоть до нескольких суток или недели. Не меньше!

Конвертация в .mp4 – первая аудиодорожка в AAC, без субтитров:

ffmpeg -i t00.mkv -c:v libx264 -crf 18 -preset veryslow -level 4.1 -g 240 -c:a libfdk_aac -b:a 512k -movflags +faststart -sn movie.mp4

Немного вдаваясь в технические детали, -g 240 определяет размер группы кадров (group of pictures) или, иными словами, вставку ключевого кадра не реже чем каждые 240 кадров, что при частоте кадров типичного фильма на Blu-Ray в 23,976 (24000/1001) примерно равно 10 секундам. Значение по умолчанию для libx264 – 250, поэтому лучше скорректировать. Это же стоит сделать и при другой частоте кадров или уменьшить в случае каких-то особо динамичных видео.

Сравнить, что получилось, по VMAF:

ffmpeg -i movie.mp4 -i t00.mkv -lavfi libvmaf -an -f null -

Первым указывается сконвертированное видео, вторым – исходное. Чем ближе к 100, тем лучше. Якобы следует стремиться к показателю в пределах 95-96 (ниже – заметна разница, выше – избыточно).

AV1 (цвет 10 бит, чтобы SVT-AV1 выдал хоть что-то на что-то похожее) + Opus:

ffmpeg -i t00.mkv -pix_fmt yuv420p10le -c:v libsvtav1 -preset 2 -crf 10 -svtav1-params "tune=0" -g 240 -c:a libopus -b:a 384k -ac 6 movie.webm

Подразумевается, что звук у первой аудиодорожки исходного файла 5.1. Проблема в том, что эти самые 5.1 бывают разные, а Opus в этом смысле оказался очень привередливым. Приходится явно указывать 6 каналов для преобразования в «правильный» звук 5.1 при необходимости.

4K HDR → 1080p SDR для последующей пересборки в MKVToolNix (на всякий случай еще раз напомню про коррекцию значения npl при необходимости):

ffmpeg -i 4K.mkv -map_metadata -1 -map 0:v:0 -filter:v "zscale=t=linear:npl=60,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,scale=1920:-2,format=yuv420p,setsar=1:1,sidedata=mode=delete" -c:v libx264 -crf 17 -preset placebo -level 4.1 -g 240 avc.sdr.1080p.mkv

Заодно убрал мастеринговые данные (sidedata=mode=delete). Но наверное HDR'у лучше оставаться HDR'ом. Посему – 4K → 1080p с обрезкой в HEVC:

ffmpeg -i 4K.mkv -map_metadata -1 -map 0:v:0 -filter:v "crop=3840:1608:0:276,scale=1920:-2,setsar=1:1" -c:v libx265 -crf 17.5 -preset slower -g 240 hevc.1080p.mkv

Если вам показалось, что «более медленный» x265 работает слишком быстро, то, во-первых, я вам искренне завидую, а во-вторых – на Вас персональный наряд на все 15 суток:

ffmpeg -i t00.mkv -map_metadata -1 -map 0:v:0 -pix_fmt yuv420p10le -c:v libvvenc -preset medium -qp 18 -vvenc-params intraperiod=240 movie.ts

Справедливости ради, -preset faster -qp 17 был бы намного быстрее и возможно лучше по качеству, но за счет битрейта.

Скриншот в формате .png:

ffmpeg -ss 1:02:34 -i t00.mkv -frames:v 1 screenshot.png

Здесь важно отметить, что в зависимости от расположения -ss (до или после входного файла) результат будет разным. Если указать «до», будет произведен быстрый поиск, но вроде бы есть вероятность, что попадание окажется приблизительным. Если же после, то по идее позиция будет найдена точно, но это требует декодирования, что на мало-мальски длинных видео займет много времени.

Сравнение со сборкой от gyan.dev

На всякий случай решил проверить, отличается ли моя сборка от «эталонной» в плане скорости кодирования. Надо сказать, gyan.dev'ы похоже берут библиотеки с «мастера», а не релизные версии, но, надеюсь, разница окажется в пределах погрешности. Взял некий фрагмент длительностью 1 минута и преобразовал его в AVC, HEVC и VVC:

ffmpeg -i int_cut.mkv -c:v libx264 -preset placebo -crf 17 -g 240 -level 4.1 int.x264.17.placebo.mkv
ffmpeg -i int_cut.mkv -c:v libx265 -preset slow -crf 17 -g 240 int.x265.17.slow.mkv
ffmpeg -i int_cut.mkv -pix_fmt yuv420p10le -c:v libvvenc -preset faster -qp 17 -period 10 int.vvc.17.faster.10bit.mkv
lib время, моя время, gyan.dev
x264 0:06:38.93 0:06:32.55
x265 0:03:26.04 0:03:15.12
vvenc 0:06:20.37 0:06:24.88

«Иксы» у меня оказались чуть медленнее, а VVenC – быстрее, но разницей в несколько секунд думаю можно пренебречь. Или в случае x265 все же нет? Можно ли ее объяснить некими оптимизациями, внесенными после выхода версии 4.3? Или повлияли «защитные» флаги компиляции от BtbN? Или система сборки у gyan.dev (MSYS2) более эффективная? Вопросы без ответов… scratch

И на этом пожалуй закончим. В следующий раз я планирую подробно изучить настройки x265 и VVenC. Именно из этого исследования я в немалой степени взял примеры, а также загадочный int_cut.mkv. А пока – эффективного кодирования и приятного просмотра!


Категория: Программирование, веб | Опубликовано 04.10.2026 | Редакция от 06.10.2026

Похожие материалы


Комментарии, обсуждение