#!/usr/bin/env bash set -euo pipefail MEDIA3_TAG='1.10.1' FFMPEG_BRANCH='release/6.0' NDK_PATH="${ANDROID_NDK_HOME:-}" HOST_PLATFORM='' CLEAN=0 ANDROID_ABI='21' ENABLED_DECODERS=(dca ac3 eac3 truehd opus vorbis flac) usage() { cat <<'USAGE' Usage: scripts/build_media3_ffmpeg.sh [options] Options: --clean 编前先清空工作目录 --media3-tag androidx/media tag (default: 1.10.1) --ffmpeg-branch FFmpeg branch (default: release/6.0) --ndk-path Android NDK 路径(推荐 r27d LTS;默认 $ANDROID_NDK_HOME) --host-platform darwin-x86_64 / linux-x86_64 (省略时根据 uname 推断;Apple Silicon 强烈建议 通过 Docker linux-x86_64 容器执行) -h, --help 显示本帮助 USAGE } while [[ $# -gt 0 ]]; do case "$1" in --clean) CLEAN=1; shift ;; --media3-tag) MEDIA3_TAG="$2"; shift 2 ;; --ffmpeg-branch) FFMPEG_BRANCH="$2"; shift 2 ;; --ndk-path) NDK_PATH="$2"; shift 2 ;; --host-platform) HOST_PLATFORM="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown option: $1" >&2; usage; exit 2 ;; esac done require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "ERROR: $1 not found in PATH" >&2 exit 1 fi } require_cmd git require_cmd shasum if [[ -z "${NDK_PATH}" ]]; then echo 'ERROR: NDK path not specified. Use --ndk-path or set ANDROID_NDK_HOME.' >&2 echo 'Pin to NDK r27d LTS (27.3.13750724) — ABI-compatible with Media3 1.10.1 build_ffmpeg.sh.' >&2 exit 1 fi if [[ ! -d "${NDK_PATH}" ]]; then echo "ERROR: NDK path not a directory: ${NDK_PATH}" >&2 exit 1 fi SDK_PATH="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" if [[ -z "${SDK_PATH}" ]]; then for candidate in \ "${HOME}/Library/Android/sdk" \ "${HOME}/Android/Sdk" \ "/opt/android-sdk" \ "/usr/local/share/android-sdk"; do if [[ -d "${candidate}/platforms" ]]; then SDK_PATH="${candidate}" echo "[detect] Android SDK at ${SDK_PATH}" break fi done fi if [[ -z "${SDK_PATH}" || ! -d "${SDK_PATH}/platforms" ]]; then echo 'ERROR: Android SDK not found. Set ANDROID_HOME or install Android Studio SDK.' >&2 echo ' Required: $ANDROID_HOME/platforms (android-34+) + build-tools.' >&2 exit 1 fi if [[ -z "${HOST_PLATFORM}" ]]; then uname_s="$(uname -s)" uname_m="$(uname -m)" case "${uname_s}-${uname_m}" in Darwin-x86_64) HOST_PLATFORM='darwin-x86_64' ;; Darwin-arm64) HOST_PLATFORM='darwin-x86_64' echo 'WARN: host is Apple Silicon; Media3 README only documents darwin-x86_64.' >&2 echo ' Build will proceed under Rosetta. For reproducibility prefer a' >&2 echo ' Docker linux-x86_64 container (see plugins/media3_engine/README.md).' >&2 ;; Linux-x86_64) HOST_PLATFORM='linux-x86_64' ;; *) echo "ERROR: unsupported host ${uname_s}-${uname_m}. Pass --host-platform." >&2; exit 1 ;; esac fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" WORK_ROOT="${REPO_ROOT}/.media3-build" MEDIA_REPO="${WORK_ROOT}/media" FFMPEG_MODULE_PATH="${MEDIA_REPO}/libraries/decoder_ffmpeg/src/main" FFMPEG_DIR="${FFMPEG_MODULE_PATH}/jni/ffmpeg" OUT_LIBS_DIR="${REPO_ROOT}/android/app/libs" AAR_NAME="media3-decoder-ffmpeg-${MEDIA3_TAG}-arm64.aar" OUT_AAR="${OUT_LIBS_DIR}/${AAR_NAME}" PATCH_BACKUP_DIR='' PATCHED_FILES=() backup_patch_file() { local file="$1" if [[ -z "${PATCH_BACKUP_DIR}" ]]; then PATCH_BACKUP_DIR="$(mktemp -d)" fi local backup="${PATCH_BACKUP_DIR}/${#PATCHED_FILES[@]}" cp "${file}" "${backup}" PATCHED_FILES+=("${file}:${backup}") } restore_patched_files() { local entry file backup for entry in "${PATCHED_FILES[@]}"; do file="${entry%%:*}" backup="${entry#*:}" if [[ -f "${backup}" ]]; then cp "${backup}" "${file}" fi done if [[ -n "${PATCH_BACKUP_DIR}" ]]; then rm -rf "${PATCH_BACKUP_DIR}" fi } trap restore_patched_files EXIT if [[ ${CLEAN} -eq 1 && -d "${WORK_ROOT}" ]]; then echo "[clean] removing ${WORK_ROOT}" rm -rf "${WORK_ROOT}" fi mkdir -p "${WORK_ROOT}" "${OUT_LIBS_DIR}" if [[ ! -d "${MEDIA_REPO}/.git" ]]; then echo "[clone] androidx/media @ ${MEDIA3_TAG}" git clone --depth 1 --branch "${MEDIA3_TAG}" \ https://github.com/androidx/media.git "${MEDIA_REPO}" else echo "[skip] ${MEDIA_REPO} already exists; pass --clean to refetch" fi if [[ ! -d "${FFMPEG_DIR}/.git" ]]; then echo "[clone] FFmpeg ${FFMPEG_BRANCH}" mkdir -p "$(dirname "${FFMPEG_DIR}")" git clone --depth 1 --branch "${FFMPEG_BRANCH}" \ https://git.ffmpeg.org/ffmpeg.git "${FFMPEG_DIR}" else echo "[skip] ${FFMPEG_DIR} already exists" fi JNI_BUILD_SH="${FFMPEG_MODULE_PATH}/jni/build_ffmpeg.sh" if [[ ! -f "${JNI_BUILD_SH}" ]]; then echo "ERROR: ${JNI_BUILD_SH} not found (androidx/media tag ${MEDIA3_TAG} 内未包含)" >&2 exit 1 fi if ! grep -q '# SM-PATCH arm64-only' "${JNI_BUILD_SH}"; then echo "[patch] forcing arm64-v8a only in ${JNI_BUILD_SH##*/}" backup_patch_file "${JNI_BUILD_SH}" python3 - "${JNI_BUILD_SH}" <<'PY' || { echo 'ERROR: failed to patch build_ffmpeg.sh (upstream layout changed?)' >&2; exit 1; } import re, sys p = sys.argv[1] src = open(p, 'r', encoding='utf-8').read() if '# SM-PATCH arm64-only' in src: sys.exit(0) KEEP = 'arm64-v8a' block_re = re.compile( r'\./configure\s*\\\s*\n' r'(?:.*\n)*?' r'\s*--libdir=android-libs/([\w-]+)\s*\\\s*\n' r'(?:.*\n)*?' r'make clean\s*\n', re.M, ) removed = [] chunks = [] last = 0 for m in block_re.finditer(src): abi = m.group(1) if abi == KEEP: continue chunks.append(src[last:m.start()]) last = m.end() removed.append(abi) chunks.append(src[last:]) if not removed: sys.stderr.write('ERROR: no non-arm64 configure blocks found; upstream layout may have changed.\n') sys.exit(2) result = ''.join(chunks).replace( 'set -eu\n', f'set -eu\n# SM-PATCH arm64-only (removed: {",".join(removed)})\n', 1, ) open(p, 'w', encoding='utf-8').write(result) sys.stderr.write(f'[ok] removed ABI blocks: {",".join(removed)}\n') PY fi echo "[build] FFmpeg native libs (decoders: ${ENABLED_DECODERS[*]})" pushd "${FFMPEG_MODULE_PATH}/jni" >/dev/null NDK_PATH="${NDK_PATH}" \ HOST_PLATFORM="${HOST_PLATFORM}" \ ANDROID_ABI="${ANDROID_ABI}" \ ./build_ffmpeg.sh \ "${FFMPEG_MODULE_PATH}" \ "${NDK_PATH}" \ "${HOST_PLATFORM}" \ "${ANDROID_ABI}" \ "${ENABLED_DECODERS[@]}" popd >/dev/null echo '[build] Gradle :libraries:decoder_ffmpeg:assembleRelease' cat >"${MEDIA_REPO}/local.properties" <&2 exit 1 fi COMMON_CONFIG="${MEDIA_REPO}/common_config.gradle" if ! grep -q '// SM-PATCH ndkVersion' "${COMMON_CONFIG}"; then echo "[patch] injecting ndkVersion=${NDK_VERSION} into common_config.gradle" backup_patch_file "${COMMON_CONFIG}" python3 - "${COMMON_CONFIG}" "${NDK_VERSION}" <<'PY' || { echo 'ERROR: failed to inject ndkVersion' >&2; exit 1; } import re, sys p, version = sys.argv[1], sys.argv[2] src = open(p, 'r', encoding='utf-8').read() patched, n = re.subn( r'^android\s*\{\s*\n', f'android {{\n ndkVersion = "{version}" // SM-PATCH ndkVersion\n', src, count=1, flags=re.M, ) if n != 1: sys.stderr.write('ERROR: failed to find `android {` in common_config.gradle\n') sys.exit(2) open(p, 'w', encoding='utf-8').write(patched) PY fi DECODER_BUILD_GRADLE="${MEDIA_REPO}/libraries/decoder_ffmpeg/build.gradle" if ! grep -q '// SM-PATCH abiFilters' "${DECODER_BUILD_GRADLE}"; then echo "[patch] restricting decoder_ffmpeg abiFilters to arm64-v8a" backup_patch_file "${DECODER_BUILD_GRADLE}" python3 - "${DECODER_BUILD_GRADLE}" <<'PY' || { echo 'ERROR: failed to inject abiFilters' >&2; exit 1; } import re, sys p = sys.argv[1] src = open(p, 'r', encoding='utf-8').read() inject = ( "android {\n" " defaultConfig {\n" " ndk {\n" " abiFilters 'arm64-v8a' // SM-PATCH abiFilters\n" " }\n" " }\n" "}\n\n" ) patched, n = re.subn( r'(apply from: "\$gradle\.ext\.androidxMediaSettingsDir/common_config\.gradle"\n)', r'\1\n' + inject, src, count=1, ) if n != 1: sys.stderr.write('ERROR: failed to find common_config apply line in decoder_ffmpeg/build.gradle\n') sys.exit(2) open(p, 'w', encoding='utf-8').write(patched) PY fi pushd "${MEDIA_REPO}" >/dev/null ANDROID_HOME="${SDK_PATH}" ANDROID_SDK_ROOT="${SDK_PATH}" \ ./gradlew --no-daemon :lib-decoder-ffmpeg:assembleRelease popd >/dev/null GRADLE_AAR='' for pattern in \ "${MEDIA_REPO}/libraries/decoder_ffmpeg/buildout/outputs/aar/lib-decoder-ffmpeg-release.aar" \ "${MEDIA_REPO}/libraries/decoder_ffmpeg/build/outputs/aar/lib-decoder-ffmpeg-release.aar" \ "${MEDIA_REPO}/libraries/decoder_ffmpeg/buildout/outputs/aar/decoder_ffmpeg-release.aar" \ "${MEDIA_REPO}/libraries/decoder_ffmpeg/build/outputs/aar/decoder_ffmpeg-release.aar"; do if [[ -f "${pattern}" ]]; then GRADLE_AAR="${pattern}"; break fi done if [[ -z "${GRADLE_AAR}" ]]; then echo 'ERROR: AAR not found under decoder_ffmpeg/{buildout,build}/outputs/aar/. 检查 Gradle 输出' >&2 exit 1 fi cp "${GRADLE_AAR}" "${OUT_AAR}" SHA256="$(shasum -a 256 "${OUT_AAR}" | awk '{print $1}')" CHECKSUMS="${OUT_LIBS_DIR}/CHECKSUMS.txt" touch "${CHECKSUMS}" python3 - "${CHECKSUMS}" "${AAR_NAME}" "${SHA256}" <<'PY' import sys, re p, name, sha = sys.argv[1], sys.argv[2], sys.argv[3] with open(p, 'r', encoding='utf-8') as f: lines = [ln for ln in f.read().splitlines() if not ln.endswith(name)] lines.append(f'{sha} {name}') with open(p, 'w', encoding='utf-8') as f: f.write('\n'.join(lines) + '\n') PY cat <