#!/bin/bash # Builds an LGPL-only Media3 FFmpeg audio decoder AAR for Zapline. # # Output: androidApp/libs/media3-decoder-ffmpeg-lgpl-${MEDIA3_VERSION}.aar # # Follows libraries/decoder_ffmpeg/README.md of androidx/media at tag ${MEDIA3_VERSION} # (FFmpeg branch release/6.0, as recommended by that README), with three changes: # * FFmpeg is configured with explicit --disable-gpl --disable-nonfree (LGPL-2.1+ only; no --enable-version3). # * Only ABIs arm64-v8a, armeabi-v7a, x86_64 are built (Media3's script also builds x86). # * The module's build.gradle.kts in the scratch checkout gets abiFilters + ndkVersion appended. # The prebuilt jellyfin "media3-ffmpeg-decoder" artifact is GPL-3.0 and must NOT be used instead. # # Requirements: git, JDK 21, Android SDK with NDK r28c and CMake 3.22.1, network access. # Usage: tools/ffmpeg/build_ffmpeg.sh (re-runnable; clones reused; an ABI is rebuilt only when its stamp changes) # DRY_RUN=1 tools/ffmpeg/build_ffmpeg.sh (print skip/rebuild decision per ABI, build nothing) # Host: macOS only (darwin-x86_64 NDK prebuilts, sysctl). set -euo pipefail MEDIA3_VERSION="1.11.1" MEDIA3_COMMIT="8c6678b657ede1e7883fc164ef73ed483c7796c3" # commit of annotated tag 1.11.1 (tag object 915b73c4) FFMPEG_BRANCH="release/6.0" FFMPEG_COMMIT="573f2b81ebd97da81def2ba762e81b0328547004" # release/6.0 head used for the shipped AAR (RELEASE=6.0.2) SDK_ROOT="${ANDROID_HOME:-/opt/homebrew/share/android-commandlinetools}" NDK_VERSION="28.2.13676358" NDK_PATH="${SDK_ROOT}/ndk/${NDK_VERSION}" HOST_PLATFORM="darwin-x86_64" # NDK ships universal (x86_64+arm64) binaries under this name on macOS ANDROID_API=24 # must be <= app minSdk (28) ABIS=(armeabi-v7a arm64-v8a x86_64) ENABLED_DECODERS=(ac3 eac3 dca truehd mlp mp2 mp3 flac vorbis opus alac pcm_s16le pcm_s24le pcm_s32le aac) export JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk@21}" export ANDROID_HOME="${SDK_ROOT}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" WORK="${SCRIPT_DIR}/work" MEDIA="${WORK}/media" FFMPEG="${WORK}/ffmpeg-6.0" MODULE="${MEDIA}/libraries/decoder_ffmpeg" JNI="${MODULE}/src/main/jni" TC="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/bin" JOBS="$(sysctl -n hw.ncpu 2>/dev/null || nproc)" mkdir -p "${WORK}" # 1. Sources [[ -d "${MEDIA}/.git" ]] || git clone --depth 1 --branch "${MEDIA3_VERSION}" https://github.com/androidx/media.git "${MEDIA}" if [[ "$(git -C "${MEDIA}" rev-parse HEAD)" != "${MEDIA3_COMMIT}" ]]; then echo "Media3 checkout is not at ${MEDIA3_COMMIT} (tag ${MEDIA3_VERSION})"; exit 1 fi echo "Media3 commit: $(git -C "${MEDIA}" rev-parse HEAD) (tag ${MEDIA3_VERSION})" [[ -d "${FFMPEG}/.git" ]] || git clone --depth 1 --branch "${FFMPEG_BRANCH}" https://github.com/FFmpeg/FFmpeg.git "${FFMPEG}" if [[ "$(git -C "${FFMPEG}" rev-parse HEAD)" != "${FFMPEG_COMMIT}" ]]; then git -C "${FFMPEG}" fetch --depth 1 origin "${FFMPEG_COMMIT}" && git -C "${FFMPEG}" checkout -q "${FFMPEG_COMMIT}" fi [[ "$(git -C "${FFMPEG}" rev-parse HEAD)" == "${FFMPEG_COMMIT}" ]] || { echo "FFmpeg checkout is not at ${FFMPEG_COMMIT}"; exit 1; } echo "FFmpeg commit: $(git -C "${FFMPEG}" rev-parse HEAD) ($(cat "${FFMPEG}/RELEASE"))" ln -sfn "${FFMPEG}" "${JNI}/ffmpeg" # 2. FFmpeg static libs (in-tree, one ABI at a time, same options as Media3's build_ffmpeg.sh) COMMON_OPTIONS=( --target-os=android --enable-static --disable-shared --disable-doc --disable-programs --disable-everything --disable-avdevice --disable-avformat --disable-swscale --disable-postproc --disable-avfilter --disable-symver --enable-swresample --extra-ldexeflags=-pie --disable-v4l2-m2m --disable-vulkan --disable-gpl --disable-nonfree ) for d in "${ENABLED_DECODERS[@]}"; do COMMON_OPTIONS+=("--enable-decoder=${d}"); done TOOLS=(--nm="${TC}/llvm-nm" --ar="${TC}/llvm-ar" --ranlib="${TC}/llvm-ranlib" --strip="${TC}/llvm-strip") cd "${FFMPEG}" # On failure while building an ABI, leave the tree clean and drop that ABI's partial libs. BUILDING="" cleanup_failed_abi() { local rc=$? if [[ ${rc} -ne 0 && -n "${BUILDING}" ]]; then echo "Build of ${BUILDING} failed (exit ${rc}); running make distclean" (cd "${FFMPEG}" && make distclean > /dev/null 2>&1 || true) rm -rf "${FFMPEG}/android-libs/${BUILDING}" fi } trap cleanup_failed_abi EXIT for abi in "${ABIS[@]}"; do case "${abi}" in armeabi-v7a) ARGS=(--arch=arm --cpu=armv7-a --cross-prefix="${TC}/armv7a-linux-androideabi${ANDROID_API}-" --extra-cflags="-march=armv7-a -mfloat-abi=softfp" --extra-ldflags="-Wl,--fix-cortex-a8") ;; arm64-v8a) ARGS=(--arch=aarch64 --cpu=armv8-a --cross-prefix="${TC}/aarch64-linux-android${ANDROID_API}-") ;; x86_64) ARGS=(--arch=x86_64 --cpu=x86-64 --cross-prefix="${TC}/x86_64-linux-android${ANDROID_API}-" --disable-asm) ;; esac CONFIGURE_ARGS=(--libdir="android-libs/${abi}" "${ARGS[@]}" "${TOOLS[@]}" "${COMMON_OPTIONS[@]}") # Stamp = sha256 of every configure arg (incl. decoder list) + FFmpeg commit + NDK version. STAMP="$(printf '%s\n' "${CONFIGURE_ARGS[@]}" "ffmpeg=${FFMPEG_COMMIT}" "ndk=${NDK_VERSION}" | shasum -a 256 | cut -d' ' -f1)" STAMP_FILE="android-libs/${abi}/.stamp" if [[ -f "android-libs/${abi}/libavcodec.a" && -f "${STAMP_FILE}" && "$(cat "${STAMP_FILE}")" == "${STAMP}" ]]; then echo "${abi}: stamp match (${STAMP:0:12}) -> skip"; continue fi echo "${abi}: stamp mismatch or missing (want ${STAMP:0:12}, have $(cat "${STAMP_FILE}" 2>/dev/null | cut -c1-12 || true)) -> rebuild" [[ "${DRY_RUN:-0}" == "1" ]] && continue BUILDING="${abi}" rm -rf "android-libs/${abi}" ./configure "${CONFIGURE_ARGS[@]}" > "${WORK}/configure-${abi}.log" grep -q '^License: LGPL version 2.1 or later' "${WORK}/configure-${abi}.log" || { echo "License is not LGPL-2.1+"; exit 1; } for flag in CONFIG_GPL CONFIG_NONFREE CONFIG_VERSION3; do grep -qx "#define ${flag} 0" config.h || { echo "config.h: ${flag} is not 0"; exit 1; } done make -j"${JOBS}" > "${WORK}/make-${abi}.log" 2>&1 make install-libs >> "${WORK}/make-${abi}.log" 2>&1 make clean > /dev/null echo "${STAMP}" > "${STAMP_FILE}" BUILDING="" done [[ "${DRY_RUN:-0}" == "1" ]] && { echo "DRY_RUN: stopping before Gradle"; exit 0; } # 3. AAR via the module's own Gradle build (CMake links libffmpegJNI.so from ffmpeg_jni.cc + static libs) GRADLE_PATCH_MARK="// zapline: abi filters" if ! grep -q "${GRADLE_PATCH_MARK}" "${MODULE}/build.gradle.kts"; then cat >> "${MODULE}/build.gradle.kts" < "${MEDIA}/local.properties" cd "${MEDIA}" ./gradlew --no-daemon -q :lib-decoder-ffmpeg:assembleRelease AAR_SRC="$(ls "${MODULE}"/buildout/outputs/aar/*release.aar 2>/dev/null || ls "${MODULE}"/build/outputs/aar/*release.aar)" mkdir -p "${PROJECT_DIR}/androidApp/libs" cp "${AAR_SRC}" "${PROJECT_DIR}/androidApp/libs/media3-decoder-ffmpeg-lgpl-${MEDIA3_VERSION}.aar" echo "AAR: ${PROJECT_DIR}/androidApp/libs/media3-decoder-ffmpeg-lgpl-${MEDIA3_VERSION}.aar"