From f77d55aaaa9d6392eee0d8bee90f55d60a05e034 Mon Sep 17 00:00:00 2001 From: Matthew Geiger Date: Wed, 30 Sep 2026 21:45:09 -0700 Subject: [PATCH 1/3] Add a portable statically linked Linux binary Releases can now ship a prebuilt binary that runs on any x86-64 Linux with glibc 2.31 or newer. - packaging/build-static-deps.sh builds FFmpeg 9.0 and its libraries (dav1d, Mbed TLS, libva, libdrm, nv-codec-headers, zlib) as static libraries from checksummed sources, and collects their licenses. - -DNDISTREAMER_STATIC=ON links them, together with libstdc++, libgcc and libatomic, so only glibc stays dynamic. glibc has to stay dynamic because the NDI runtime is loaded with dlopen. - packaging/package.sh checks that only glibc is needed, then builds the release archive with third-party notices, license texts and a tarball of the exact third-party sources (LGPLv3 for FFmpeg). - The release workflow builds the binary in an Ubuntu 20.04 container, runs the test suite against it, smoke-tests it on Ubuntu, Debian, Rocky Linux, Fedora and Arch, and attaches it to the release. - FFmpeg built with Mbed TLS has no default CA store, so HTTPS inputs now use the system CA bundle unless -o ca_file is given. Bumps the version to 2.0.1. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 166 ++++++++++++++++++++++++++++++++ CHANGELOG.md | 12 +++ CMakeLists.txt | 21 +++- README.md | 33 +++++++ packaging/build-static-deps.sh | 171 +++++++++++++++++++++++++++++++++ packaging/package.sh | 113 ++++++++++++++++++++++ src/av/input.cpp | 35 +++++++ tests/CMakeLists.txt | 2 +- tests/test_input.cpp | 7 ++ 9 files changed, 557 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100755 packaging/build-static-deps.sh create mode 100755 packaging/package.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2969b8f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,166 @@ +name: Release binaries + +# Builds the portable Linux binary, checks that it runs on several distributions, and attaches it +# to the GitHub release. Pull requests that touch the packaging build and test it without publishing. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Existing release tag to attach the binary to (leave empty to only build)" + required: false + pull_request: + paths: + - "packaging/**" + - ".github/workflows/release.yml" + - "CMakeLists.txt" + +permissions: + contents: write + +jobs: + build: + name: Build portable x86_64 binary + runs-on: ubuntu-24.04 + # An old glibc (2.31) as the baseline: the binary runs on Ubuntu 20.04, Debian 11, RHEL 9 and newer. + container: ubuntu:20.04 + env: + DEBIAN_FRONTEND: noninteractive + DEPS: /opt/ndistreamer-deps + steps: + - name: Install build tools + run: | + apt-get update + apt-get install -y --no-install-recommends software-properties-common gpg-agent ca-certificates \ + curl git xz-utils bzip2 make nasm ninja-build pkg-config python3-pip binutils file + add-apt-repository -y ppa:ubuntu-toolchain-r/test + apt-get update + apt-get install -y --no-install-recommends gcc-13 g++-13 + update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-13 100 + update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-13 100 + update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 + update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 100 + pip3 install --no-cache-dir meson cmake + + - uses: actions/checkout@v4 + + - name: Cache static dependencies + id: cache + uses: actions/cache@v4 + with: + path: /opt/ndistreamer-deps + key: static-deps-focal-x86_64-${{ hashFiles('packaging/build-static-deps.sh') }} + + - name: Build static dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: packaging/build-static-deps.sh "$DEPS" + + - name: Build GoogleTest + run: | + git clone --quiet --depth 1 --branch v1.15.2 https://github.com/google/googletest /tmp/googletest + cmake -S /tmp/googletest -B /tmp/googletest/build -G Ninja -DCMAKE_INSTALL_PREFIX=/opt/googletest + cmake --build /tmp/googletest/build + cmake --install /tmp/googletest/build + + - name: Configure + run: > + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + -DNDISTREAMER_STATIC=ON -DNDISTREAMER_WERROR=ON + "-DCMAKE_PREFIX_PATH=$DEPS;/opt/googletest" + + - name: Build + run: cmake --build build + + - name: Test + run: ctest --test-dir build --output-on-failure --timeout 120 + + - name: Package + run: packaging/package.sh build/ndistreamer "$DEPS" dist + + - uses: actions/upload-artifact@v4 + with: + name: release + path: dist/ + + # The fake NDI runtime and a clip let the smoke tests stream without the real NDI runtime. + - name: Collect smoke test files + run: | + mkdir -p smoke + cp build/tests/fake_ndi/libndi.so.6 build/tests/media/av.mp4 smoke/ + + - uses: actions/upload-artifact@v4 + with: + name: smoke-test-files + path: smoke/ + + smoke-test: + name: Run on ${{ matrix.image }} + needs: build + runs-on: ubuntu-24.04 + container: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: + include: + - image: ubuntu:20.04 + setup: apt-get update && apt-get install -y --no-install-recommends ca-certificates + - image: debian:11 + setup: apt-get update && apt-get install -y --no-install-recommends ca-certificates + - image: rockylinux:9 + setup: "true" + - image: fedora:latest + setup: "true" + - image: archlinux:latest + setup: "true" + steps: + - name: Prepare + run: ${{ matrix.setup }} + + - uses: actions/download-artifact@v4 + with: + name: release + path: dist + + - uses: actions/download-artifact@v4 + with: + name: smoke-test-files + path: smoke + + - name: Run + shell: bash + run: | + set -euxo pipefail + tar -xzf dist/ndistreamer-*-linux-x86_64.tar.gz + bin=$(echo ndistreamer-*-linux-x86_64/ndistreamer) + "$bin" --version + + # Without the NDI runtime it must fail cleanly and say why. + if "$bin" -i smoke/av.mp4 --ndi-lib /nonexistent/libndi.so.6 2> err.txt; then exit 1; fi + grep "cannot load the NDI runtime" err.txt + + # Decode and send a local file, then a file over HTTPS (system CA bundle detection). + export FAKE_NDI_LOG=$PWD/sent.log + "$bin" -i smoke/av.mp4 --ndi-lib smoke/libndi.so.6 --no-pacing + test "$(grep -c '^video' sent.log)" -eq 50 + "$bin" -i https://castus-public-resources.s3.us-west-2.amazonaws.com/content/videos/mp4/rickroll.mp4 \ + --ndi-lib smoke/libndi.so.6 --no-pacing + test "$(grep -c '^video' sent.log)" -gt 100 + + publish: + name: Attach to release + needs: smoke-test + if: github.event_name == 'release' || inputs.tag != '' + runs-on: ubuntu-24.04 + steps: + - uses: actions/download-artifact@v4 + with: + name: release + path: dist + + - name: Upload release assets + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name || inputs.tag }} + run: gh release upload "$TAG" dist/* --clobber --repo "$GITHUB_REPOSITORY" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b3ba74..b5a0648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 2.0.1 + +- **Prebuilt portable binary.** Releases now include `ndistreamer-VERSION-linux-x86_64.tar.gz`. It + runs on any x86-64 Linux with glibc 2.31 or newer (Ubuntu 20.04, Debian 11, RHEL 9, Fedora, Arch + and later). FFmpeg 9.0 and its libraries are linked in statically, with software decoding of all + common codecs including AV1, VAAPI and NVDEC hardware decoding, HTTPS, and V4L2 capture. CI + checks that it runs on Ubuntu, Debian, Rocky Linux, Fedora and Arch. +- New `-DNDISTREAMER_STATIC=ON` build option and `packaging/` scripts to produce the portable + build yourself. +- HTTPS inputs work with FFmpeg builds that use Mbed TLS: the system CA bundle is found + automatically. + ## 2.0.0 A ground-up rewrite. The goal is unchanged (stream media as an NDI source from Linux), but the diff --git a/CMakeLists.txt b/CMakeLists.txt index 1697e51..be817b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.20) project(ndistreamer - VERSION 2.0.0 + VERSION 2.0.1 DESCRIPTION "Stream any media file, network stream or capture device as an NDI source" HOMEPAGE_URL "https://github.com/lowlevelmetal/ndi-streamer" LANGUAGES CXX) @@ -20,6 +20,8 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) option(NDISTREAMER_BUILD_TESTS "Build the test suite (requires GoogleTest)" ${PROJECT_IS_TOP_LEVEL}) option(NDISTREAMER_SANITIZE "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF) option(NDISTREAMER_WERROR "Treat compiler warnings as errors" OFF) +option(NDISTREAMER_STATIC + "Link FFmpeg and the C++ runtime statically for a portable binary (see packaging/build-static-deps.sh)" OFF) set(NDI_SDK_DIR "" CACHE PATH "Optional NDI SDK root. Not needed to build; enables ABI verification and end-to-end tests") @@ -37,6 +39,21 @@ pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET libswresample>=4) find_package(Threads REQUIRED) +if(NDISTREAMER_STATIC) + # Static FFmpeg, e.g. from packaging/build-static-deps.sh. Only glibc stays dynamic: the NDI + # runtime is loaded with dlopen, which a fully static executable cannot do. + # libatomic is a GCC runtime library that not every system has, so link its archive too. + list(TRANSFORM FFMPEG_STATIC_LDFLAGS REPLACE "^-latomic$" "-l:libatomic.a") + add_library(ndistreamer_ffmpeg INTERFACE) + target_include_directories(ndistreamer_ffmpeg SYSTEM INTERFACE ${FFMPEG_STATIC_INCLUDE_DIRS}) + target_compile_options(ndistreamer_ffmpeg INTERFACE ${FFMPEG_STATIC_CFLAGS_OTHER}) + target_link_libraries(ndistreamer_ffmpeg INTERFACE ${FFMPEG_STATIC_LDFLAGS}) + set(NDISTREAMER_FFMPEG ndistreamer_ffmpeg) + add_link_options(-static-libstdc++ -static-libgcc) +else() + set(NDISTREAMER_FFMPEG PkgConfig::FFMPEG) +endif() + # Newer FFmpeg lets filter sources carry color properties. include(CheckCXXSourceCompiles) set(CMAKE_REQUIRED_INCLUDES ${FFMPEG_INCLUDE_DIRS}) @@ -83,7 +100,7 @@ target_include_directories(ndistreamer_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_BINARY_DIR}/generated) target_link_libraries(ndistreamer_core - PUBLIC PkgConfig::FFMPEG Threads::Threads ${CMAKE_DL_LIBS} + PUBLIC ${NDISTREAMER_FFMPEG} Threads::Threads ${CMAKE_DL_LIBS} PRIVATE ndistreamer_warnings) target_compile_definitions(ndistreamer_core PRIVATE $<$:NDISTREAMER_HAVE_BUFFERSRC_COLOR>) diff --git a/README.md b/README.md index 5e3cfbf..3fd7252 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,23 @@ built for servers, signage players and test rigs, with no GUI and no X server ne - **Well behaved.** Ctrl+C or SIGTERM stops cleanly, errors are reported clearly, exit codes are meaningful, and `--stats` gives periodic statistics. +## Download + +Each [release](https://github.com/lowlevelmetal/ndi-streamer/releases) has a prebuilt binary for +x86-64 Linux that runs on any distribution with glibc 2.31 or newer (Ubuntu 20.04, Debian 11, +RHEL 9, Fedora, Arch and later): + +```sh +tar -xzf ndistreamer-*-linux-x86_64.tar.gz +./ndistreamer-*-linux-x86_64/ndistreamer --version +``` + +FFmpeg is built in, so all you need is the NDI runtime (see [Installing the NDI +runtime](#installing-the-ndi-runtime)). The binary decodes all common codecs (AV1 included), supports +VAAPI and NVDEC hardware decoding, and reads files, network streams (including HTTPS) and V4L2 +devices. X11 screen capture and ALSA/PulseAudio input need the system's libraries; for those, build +from source. + ## Requirements - Linux on x86-64 or ARM @@ -64,6 +81,22 @@ cmake --build build sudo cmake --install build # optional: installs to /usr/local/bin/ndistreamer ``` +### Portable static build + +To build the same kind of binary as the releases, compile the static dependencies once, then point +CMake at them: + +```sh +packaging/build-static-deps.sh ~/ndistreamer-deps # needs meson, ninja, nasm and cmake +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DNDISTREAMER_STATIC=ON \ + -DCMAKE_PREFIX_PATH=~/ndistreamer-deps +cmake --build build +packaging/package.sh build/ndistreamer ~/ndistreamer-deps dist # optional: release archive +``` + +The binary links only against glibc, so build on the oldest distribution you want to support. The +releases are built on Ubuntu 20.04. + ### Installing the NDI runtime Download the NDI SDK for Linux and run its installer. It unpacks into `NDI SDK for Linux/`. Then do diff --git a/packaging/build-static-deps.sh b/packaging/build-static-deps.sh new file mode 100755 index 0000000..afd4dbb --- /dev/null +++ b/packaging/build-static-deps.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +# Builds FFmpeg and the libraries it uses as static libraries, so ndistreamer can be linked into a +# portable binary that depends only on glibc (configure it with -DNDISTREAMER_STATIC=ON). +# +# Usage: packaging/build-static-deps.sh PREFIX +# +# Needs a C compiler, make, cmake, meson, ninja, nasm, pkg-config, curl and tar. Sources are +# downloaded into $NDISTREAMER_DOWNLOAD_DIR (default: PREFIX/src) and verified against SHA-256 sums. +# License texts and a manifest of what was built go to PREFIX/share/ndistreamer-deps. +# +# Included: FFmpeg's built-in codecs, formats and protocols; AV1 software decoding (dav1d); TLS for +# https and friends (Mbed TLS); VAAPI and NVDEC hardware decoding; V4L2 capture. +# Not included: X11 screen capture and ALSA/PulseAudio input, which need the system's libraries +# (build against your distribution's FFmpeg for those). + +set -euo pipefail + +if [ $# -ne 1 ]; then + echo "usage: $0 PREFIX" >&2 + exit 2 +fi + +prefix=$(realpath -m "$1") +downloads=${NDISTREAMER_DOWNLOAD_DIR:-$prefix/src} +build=$prefix/build +share=$prefix/share/ndistreamer-deps +jobs=$(nproc) + +# name version sha256 url license-file(s, relative to the source tree) +packages=( + "zlib 1.3.2 d7a0654783a4da529d1bb793b7ad9c3318020af77667bcae35f95d0e42a792f3 https://zlib.net/zlib-1.3.2.tar.xz" + "nv-codec-headers 11.1.5.3 2974b91062197e0527dffa3aadd8fe3bfa6681ae45f5ff9181bc0ca6479abd59 https://github.com/FFmpeg/nv-codec-headers/releases/download/n11.1.5.3/nv-codec-headers-11.1.5.3.tar.gz" + "libdrm 2.4.134 ac5e74d157830eb8bee44c6a6bf3ad49774ef0dd2a72bdad74a8f20308b52a95 https://dri.freedesktop.org/libdrm/libdrm-2.4.134.tar.xz" + "libva 2.24.1 eec6050b52876f229bd35e9df17cd31a06785e18e6f7990c445b584628483d67 https://github.com/intel/libva/releases/download/2.24.1/libva-2.24.1.tar.bz2" + "dav1d 1.5.4 686616b7c69eb88d44459391ab25cac13b6647a3b288835c5784e71c1514a5c5 https://downloads.videolan.org/pub/videolan/dav1d/1.5.4/dav1d-1.5.4.tar.xz" + "mbedtls 3.6.7 a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6 https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-3.6.7/mbedtls-3.6.7.tar.bz2" + "ffmpeg 9.0.2 8c3850283eb25fa026482078a04051e0be17347b09ef81a0849bec15a96e002e https://ffmpeg.org/releases/ffmpeg-9.0.2.tar.xz" +) + +# Where the statically linked libva looks for VA drivers (the system's); LIBVA_DRIVERS_PATH overrides. +va_driver_path=/usr/lib/x86_64-linux-gnu/dri:/usr/lib64/dri:/usr/lib/dri:/usr/local/lib/dri:/usr/lib/aarch64-linux-gnu/dri + +mkdir -p "$prefix" "$downloads" "$build" "$share/licenses" +: > "$share/manifest.tsv" + +# Only our own packages are visible to pkg-config, so nothing is picked up from the system by accident. +export PKG_CONFIG_LIBDIR=$prefix/lib/pkgconfig +export PKG_CONFIG_PATH= +export CFLAGS="-O2 -fPIC" +export CXXFLAGS="-O2 -fPIC" + +log() { + echo "==> $*" +} + +# fetch NAME: download (if needed), verify and unpack a package; prints its source directory. +fetch() { + local entry name version sha url file dir + for entry in "${packages[@]}"; do + read -r name version sha url <<<"$entry" + [ "$name" = "$1" ] && break + done + file=$downloads/$(basename "$url") + if [ ! -f "$file" ] || ! echo "$sha $file" | sha256sum -c --status; then + log "downloading $name $version" >&2 + curl -fsSL --retry 3 -o "$file.part" "$url" + mv "$file.part" "$file" + fi + echo "$sha $file" | sha256sum -c --quiet >&2 + + dir=$build/$name-$version + rm -rf "$dir" + mkdir -p "$dir" + tar -xf "$file" -C "$dir" --strip-components=1 + printf '%s\t%s\t%s\t%s\n' "$name" "$version" "$url" "$sha" >> "$share/manifest.tsv" + echo "$dir" +} + +meson_static() { + local src=$1 + shift + meson setup "$src/_build" "$src" --prefix="$prefix" --libdir=lib --buildtype=release \ + -Ddefault_library=static "$@" + meson install -C "$src/_build" +} + +build_zlib() { + local src + src=$(fetch zlib) + (cd "$src" && ./configure --prefix="$prefix" --static && make -j"$jobs" && make install) + cp "$src/LICENSE" "$share/licenses/zlib.txt" +} + +build_nv_codec_headers() { + local src + src=$(fetch nv-codec-headers) + make -C "$src" PREFIX="$prefix" install + # MIT notice from the headers themselves; there is no separate license file. + sed -n '/\/\*/,/\*\//p' "$src/include/ffnvcodec/dynlink_loader.h" | head -n 30 > "$share/licenses/nv-codec-headers.txt" +} + +build_libdrm() { + local src + src=$(fetch libdrm) + meson_static "$src" -Dintel=disabled -Dradeon=disabled -Damdgpu=disabled -Dnouveau=disabled \ + -Dvmwgfx=disabled -Domap=disabled -Dexynos=disabled -Dfreedreno=disabled -Dtegra=disabled \ + -Dvc4=disabled -Detnaviv=disabled -Dcairo-tests=disabled -Dman-pages=disabled -Dvalgrind=disabled \ + -Dtests=false -Dinstall-test-programs=false -Dudev=false + # MIT notice from the sources; there is no separate license file. + sed -n '/Copyright 1999/,/\*\//p' "$src/xf86drm.c" > "$share/licenses/libdrm.txt" +} + +build_libva() { + local src + src=$(fetch libva) + # libva only knows how to build shared libraries. + sed -i 's/shared_library(/library(/' "$src/va/meson.build" + meson_static "$src" -Dwith_x11=no -Dwith_glx=no -Dwith_wayland=no -Denable_docs=false \ + -Ddriverdir="$va_driver_path" + cp "$src/COPYING" "$share/licenses/libva.txt" +} + +build_dav1d() { + local src + src=$(fetch dav1d) + meson_static "$src" -Denable_tools=false -Denable_tests=false -Denable_examples=false + cp "$src/COPYING" "$share/licenses/dav1d.txt" +} + +build_mbedtls() { + local src + src=$(fetch mbedtls) + cmake -S "$src" -B "$src/_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$prefix" \ + -DCMAKE_INSTALL_LIBDIR=lib -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DENABLE_PROGRAMS=OFF \ + -DENABLE_TESTING=OFF -DUSE_SHARED_MBEDTLS_LIBRARY=OFF -DUSE_STATIC_MBEDTLS_LIBRARY=ON + cmake --build "$src/_build" + cmake --install "$src/_build" + cp "$src/LICENSE" "$share/licenses/mbedtls.txt" +} + +build_ffmpeg() { + local src + src=$(fetch ffmpeg) + local flags=( + --prefix="$prefix" + --pkg-config-flags=--static + --extra-cflags="-I$prefix/include" + --extra-ldflags="-L$prefix/lib" + --enable-static --disable-shared --enable-pic + --disable-autodetect --disable-doc --disable-debug --disable-ffplay --disable-ffprobe + --enable-version3 + --enable-zlib --enable-libdav1d --enable-mbedtls + --enable-vaapi --enable-ffnvcodec --enable-nvdec + ) + (cd "$src" && ./configure "${flags[@]}" && make -j"$jobs" && make install) + cp "$src/COPYING.LGPLv3" "$share/licenses/ffmpeg-LGPLv3.txt" + cp "$src/LICENSE.md" "$share/licenses/ffmpeg-LICENSE.md" + printf '%s\n' "${flags[@]:4}" > "$share/ffmpeg-configuration.txt" +} + +build_zlib +build_nv_codec_headers +build_libdrm +build_libva +build_dav1d +build_mbedtls +build_ffmpeg + +# Keep the downloads, drop the build trees. +[ "${NDISTREAMER_KEEP_BUILD:-0}" = 1 ] || rm -rf "$build" +log "done: configure ndistreamer with -DNDISTREAMER_STATIC=ON -DCMAKE_PREFIX_PATH=$prefix" diff --git a/packaging/package.sh b/packaging/package.sh new file mode 100755 index 0000000..2032ae2 --- /dev/null +++ b/packaging/package.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# Packages a portable ndistreamer build for release. +# +# Usage: packaging/package.sh BINARY DEPS_PREFIX OUTPUT_DIR +# +# BINARY ndistreamer built with -DNDISTREAMER_STATIC=ON +# DEPS_PREFIX the prefix given to packaging/build-static-deps.sh +# OUTPUT_DIR where the archives are written +# +# Fails if the binary depends on anything besides glibc. Produces: +# ndistreamer-VERSION-linux-ARCH.tar.gz the binary, docs and all license texts +# ndistreamer-VERSION-third-party-sources.tar the exact sources of the bundled libraries +# SHA256SUMS + +set -euo pipefail + +if [ $# -ne 3 ]; then + echo "usage: $0 BINARY DEPS_PREFIX OUTPUT_DIR" >&2 + exit 2 +fi + +binary=$(realpath "$1") +deps=$(realpath "$2") +output=$(realpath -m "$3") +root=$(cd "$(dirname "$0")/.." && pwd) +share=$deps/share/ndistreamer-deps + +version=$("$binary" --version | head -n 1 | awk '{print $2}') +arch=$(uname -m) +name=ndistreamer-$version-linux-$arch + +# Only glibc may remain dynamic. +allowed='^(libc\.so\.6|libm\.so\.6|libdl\.so\.2|libpthread\.so\.0|librt\.so\.1|ld-linux.*\.so\.[0-9]+)$' +needed=$(readelf -d "$binary" | sed -n 's/.*(NEEDED).*\[\(.*\)\]/\1/p') +unexpected=$(grep -Ev "$allowed" <<<"$needed" || true) +if [ -n "$unexpected" ]; then + echo "error: $binary depends on non-glibc libraries:" >&2 + echo "$unexpected" >&2 + exit 1 +fi +glibc=$(objdump -T "$binary" | grep -oE 'GLIBC_[0-9]+(\.[0-9]+)+' | sed 's/GLIBC_//' | sort -V | tail -n 1) +echo "==> $binary needs only glibc $glibc or newer" + +license_of() { + case $1 in + ffmpeg) echo "LGPL-3.0-or-later" ;; + dav1d) echo "BSD-2-Clause" ;; + mbedtls) echo "Apache-2.0" ;; + zlib) echo "Zlib" ;; + *) echo "MIT" ;; + esac +} + +staging=$(mktemp -d) +trap 'rm -rf "$staging"' EXIT +dir=$staging/$name +mkdir -p "$dir/licenses" "$output" + +cp "$binary" "$dir/ndistreamer" +strip "$dir/ndistreamer" +cp "$root/README.md" "$root/CHANGELOG.md" "$dir/" +cp "$root/LICENSE" "$dir/LICENSE" +cp "$share"/licenses/* "$dir/licenses/" + +{ + cat <"$dir/THIRD_PARTY_NOTICES.md" + +tar -C "$staging" -czf "$output/$name.tar.gz" --owner=0 --group=0 "$name" + +sources=() +while IFS=$'\t' read -r _ _ url _; do + sources+=("$(basename "$url")") +done <"$share/manifest.tsv" +tar -C "${NDISTREAMER_DOWNLOAD_DIR:-$deps/src}" -cf "$output/ndistreamer-$version-third-party-sources.tar" \ + --owner=0 --group=0 "${sources[@]}" + +(cd "$output" && sha256sum "$name.tar.gz" "ndistreamer-$version-third-party-sources.tar" >SHA256SUMS) +echo "==> wrote $output/$name.tar.gz" +ls -la "$output" diff --git a/src/av/input.cpp b/src/av/input.cpp index 5b49c33..656f035 100644 --- a/src/av/input.cpp +++ b/src/av/input.cpp @@ -8,11 +8,14 @@ #include #include +#include #include #include #include #include +#include + extern "C" { #include #include @@ -28,6 +31,30 @@ const char *MediaTypeName(AVMediaType type) { return type == AVMEDIA_TYPE_VIDEO ? "a video" : "an audio"; } +// FFmpeg built with Mbed TLS (as the portable release binary is) has no default certificate store, +// so it is pointed at the system's CA bundle unless the user chose one with -o ca_file=... +const char *SystemCaBundle() { + static const char *const kBundles[] = { + "/etc/ssl/certs/ca-certificates.crt", // Debian, Ubuntu, Arch, Alpine, Gentoo + "/etc/pki/tls/certs/ca-bundle.crt", // Fedora, RHEL + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // Fedora, RHEL + "/etc/ssl/ca-bundle.pem", // openSUSE + "/etc/ssl/cert.pem", + }; + static const char *const bundle = [] { + for (const char *path : kBundles) { + if (access(path, R_OK) == 0) return path; + } + return static_cast(nullptr); + }(); + return bundle; +} + +bool UsesMbedTls() { + static const bool uses = std::strstr(avformat_configuration(), "--enable-mbedtls") != nullptr; + return uses; +} + std::string DescribeCodec(const AVCodecParameters *par) { std::string text = avcodec_get_name(par->codec_id); if (const char *profile = avcodec_profile_name(par->codec_id, par->profile)) { @@ -70,6 +97,13 @@ void MediaInput::Open(const InputConfig &config) { for (const auto &[key, value] : config.options) { av_dict_set(&options, key.c_str(), value.c_str(), 0); } + bool default_ca_file = false; + if (UsesMbedTls() && !av_dict_get(options, "ca_file", nullptr, 0)) { + if (const char *bundle = SystemCaBundle()) { + av_dict_set(&options, "ca_file", bundle, 0); + default_ca_file = true; + } + } // On failure avformat_open_input frees the context itself. int ret = avformat_open_input(&ctx, config.url.c_str(), format, &options); @@ -81,6 +115,7 @@ void MediaInput::Open(const InputConfig &config) { const AVDictionaryEntry *unused = nullptr; while ((unused = av_dict_iterate(options, unused))) { + if (default_ca_file && std::strcmp(unused->key, "ca_file") == 0) continue; // only used for TLS log::Warn("input option '{}' was not recognized", unused->key); } av_dict_free(&options); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 37d32e5..d0b6043 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -86,7 +86,7 @@ if(NDISDK_FOUND) # Compiling this verifies src/ndi/abi.hpp against the official headers. add_executable(ndi_abi_test ndi_abi_test.cpp) target_include_directories(ndi_abi_test PRIVATE ${PROJECT_SOURCE_DIR}/src ${NDISDK_INCLUDE_DIR}) - target_link_libraries(ndi_abi_test PRIVATE PkgConfig::FFMPEG) + target_link_libraries(ndi_abi_test PRIVATE ${NDISTREAMER_FFMPEG}) add_test(NAME ndi_abi COMMAND ndi_abi_test) add_executable(ndi_receiver e2e/ndi_receiver.cpp) diff --git a/tests/test_input.cpp b/tests/test_input.cpp index ed20404..511d353 100644 --- a/tests/test_input.cpp +++ b/tests/test_input.cpp @@ -121,3 +121,10 @@ TEST(MediaInput, UnknownInputOptionsAreNotFatal) { config.options.emplace_back("no_such_option", "1"); EXPECT_NO_THROW(av::MediaInput(config, std::stop_token{})); } + +TEST(MediaInput, OpensLocalFilesWithoutWarnings) { + REQUIRE_MEDIA("av.mp4"); + testing::internal::CaptureStderr(); + av::MediaInput input(Config("av.mp4"), std::stop_token{}); + EXPECT_EQ(testing::internal::GetCapturedStderr(), ""); +} From 690e952058f4e0df13acfb0295d98c644b8c526a Mon Sep 17 00:00:00 2001 From: Matthew Geiger Date: Wed, 30 Sep 2026 21:52:34 -0700 Subject: [PATCH 2/3] Build a recent nasm as part of the static dependencies FFmpeg 9's x86 assembly does not assemble with nasm 2.14 (Ubuntu 20.04), so build-static-deps.sh now builds a pinned nasm 2.16.03 first. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 2 +- packaging/build-static-deps.sh | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2969b8f..ff079c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: run: | apt-get update apt-get install -y --no-install-recommends software-properties-common gpg-agent ca-certificates \ - curl git xz-utils bzip2 make nasm ninja-build pkg-config python3-pip binutils file + curl git xz-utils bzip2 make ninja-build pkg-config python3-pip binutils file add-apt-repository -y ppa:ubuntu-toolchain-r/test apt-get update apt-get install -y --no-install-recommends gcc-13 g++-13 diff --git a/packaging/build-static-deps.sh b/packaging/build-static-deps.sh index afd4dbb..41fb623 100755 --- a/packaging/build-static-deps.sh +++ b/packaging/build-static-deps.sh @@ -4,7 +4,8 @@ # # Usage: packaging/build-static-deps.sh PREFIX # -# Needs a C compiler, make, cmake, meson, ninja, nasm, pkg-config, curl and tar. Sources are +# Needs a C compiler, make, cmake, meson, ninja, pkg-config, curl and tar. A recent nasm (which +# FFmpeg's assembly requires) is built along the way. Sources are # downloaded into $NDISTREAMER_DOWNLOAD_DIR (default: PREFIX/src) and verified against SHA-256 sums. # License texts and a manifest of what was built go to PREFIX/share/ndistreamer-deps. # @@ -26,8 +27,9 @@ build=$prefix/build share=$prefix/share/ndistreamer-deps jobs=$(nproc) -# name version sha256 url license-file(s, relative to the source tree) +# name version sha256 url packages=( + "nasm 2.16.03 1412a1c760bbd05db026b6c0d1657affd6631cd0a63cddb6f73cc6d4aa616148 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/nasm-2.16.03.tar.xz" "zlib 1.3.2 d7a0654783a4da529d1bb793b7ad9c3318020af77667bcae35f95d0e42a792f3 https://zlib.net/zlib-1.3.2.tar.xz" "nv-codec-headers 11.1.5.3 2974b91062197e0527dffa3aadd8fe3bfa6681ae45f5ff9181bc0ca6479abd59 https://github.com/FFmpeg/nv-codec-headers/releases/download/n11.1.5.3/nv-codec-headers-11.1.5.3.tar.gz" "libdrm 2.4.134 ac5e74d157830eb8bee44c6a6bf3ad49774ef0dd2a72bdad74a8f20308b52a95 https://dri.freedesktop.org/libdrm/libdrm-2.4.134.tar.xz" @@ -53,7 +55,8 @@ log() { echo "==> $*" } -# fetch NAME: download (if needed), verify and unpack a package; prints its source directory. +# fetch NAME [tool]: download (if needed), verify and unpack a package; prints its source directory. +# Packages that end up in the binary are recorded in the manifest; build tools are not. fetch() { local entry name version sha url file dir for entry in "${packages[@]}"; do @@ -72,7 +75,9 @@ fetch() { rm -rf "$dir" mkdir -p "$dir" tar -xf "$file" -C "$dir" --strip-components=1 - printf '%s\t%s\t%s\t%s\n' "$name" "$version" "$url" "$sha" >> "$share/manifest.tsv" + if [ "${2:-}" != tool ]; then + printf '%s\t%s\t%s\t%s\n' "$name" "$version" "$url" "$sha" >> "$share/manifest.tsv" + fi echo "$dir" } @@ -84,6 +89,14 @@ meson_static() { meson install -C "$src/_build" } +# FFmpeg's assembly needs a newer nasm than many distributions ship. +build_nasm() { + local src + src=$(fetch nasm tool) + (cd "$src" && ./configure --prefix="$prefix/tools" && make -j"$jobs" nasm ndisasm && make install) + export PATH=$prefix/tools/bin:$PATH +} + build_zlib() { local src src=$(fetch zlib) @@ -158,6 +171,7 @@ build_ffmpeg() { printf '%s\n' "${flags[@]:4}" > "$share/ffmpeg-configuration.txt" } +build_nasm build_zlib build_nv_codec_headers build_libdrm From 71c6881e47d71ede562380f6895b6895b0a9c25d Mon Sep 17 00:00:00 2001 From: Matthew Geiger Date: Wed, 30 Sep 2026 22:02:22 -0700 Subject: [PATCH 3/3] Smoke-test the portable binary on Debian 12 Debian 11 is end-of-life and its security mirror no longer serves the packages the test installs. Ubuntu 20.04 still covers the glibc 2.31 baseline. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff079c8..aef6e1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,7 +106,7 @@ jobs: include: - image: ubuntu:20.04 setup: apt-get update && apt-get install -y --no-install-recommends ca-certificates - - image: debian:11 + - image: debian:12 setup: apt-get update && apt-get install -y --no-install-recommends ca-certificates - image: rockylinux:9 setup: "true"