#!/usr/bin/env bash
# MaugPelis (audio AC-3, E-AC-3 y DTS): compila la extensión de FFmpeg de Media3 con FFmpeg de
# licencia LGPL y deja lo que usa la app:
#   app/libs/media3-decoder-ffmpeg-<Media3>.aar (+ .sha256)   el AAR (clases y librerías nativas)
#   app/src/main/assets/licencias/FFmpeg.txt                    aviso: copyright, versión, commit,
#                                                               opciones y dónde está el código
#   app/src/main/assets/licencias/LGPL-2.1.txt                  el texto de la LGPL (de FFmpeg)
#   app/src/main/assets/licencias/LICENSE-APACHE-2.0.txt        el de la Apache 2.0 (de Media3)
#   tools/ffmpeg/codigo-abierto/ffmpeg_jni.cc                   el puente de Media3, tal cual
# La LGPL pide ofrecer el código fuente: la página es https://maugpelis.com/codigo-abierto y los
# archivos van sueltos en https://maugpelis.com/codigo-abierto/ffmpeg/ (ffmpeg-<versión>.tar.xz,
# build.sh, FFmpeg.txt, ffmpeg_jni.cc y LICENSE-APACHE-2.0.txt; el de FFmpeg se rehace con git
# archive del commit fijado, ver FFmpeg.txt).
#
# Se corre en GitHub Actions (flujo "FFmpeg (audio Dolby y DTS)", .github/workflows/ffmpeg-audio.yml).
# También en Linux o macOS con el SDK de Android (ANDROID_HOME), Java 21, git, xz y Python 3:
#   bash tools/ffmpeg/build.sh                    compila y deja todo en tools/ffmpeg/build/
#   bash tools/ffmpeg/build.sh --instalar         además lo copia a app/libs, assets/licencias y
#                                                 tools/ffmpeg/codigo-abierto
#   bash tools/ffmpeg/build.sh probar             después de compilar: prueba el AAR en un emulador
#                                                 Android 14 x86_64 (Linux con KVM)
#   bash tools/ffmpeg/build.sh fuente             solo arma lo que se publica en la web, en
#                                                 tools/ffmpeg/build/codigo-abierto/ffmpeg/
#   bash tools/ffmpeg/build.sh licencias FF M3    solo los textos de licencias (FF: el código de
#                                                 FFmpeg; M3: el de androidx/media)
#
# De dónde sale cada cosa (sin parches):
# - FFmpeg: la etiqueta n<versión> del espejo oficial en GitHub, comprobada contra el commit fijado
#   abajo. Se compila el árbol de git archive de ese commit: lo mismo que se publica.
# - El puente (ffmpeg_jni.cc, Apache 2.0) y las clases de Media3: androidx/media en el commit fijado.
#
# Qué sale:
# - FFmpeg como bibliotecas compartidas (libavutil.so, libswresample.so, libavcodec.so) que
#   libffmpegJNI.so carga por su nombre: la LGPL pide que se puedan cambiar por otra versión. Solo
#   LGPL (sin --enable-gpl, --enable-version3 ni --enable-nonfree: se comprueba en config.h) y solo
#   los decodificadores de audio que muchos teléfonos no traen: AC-3, E-AC-3, DTS (dca, también
#   DTS-HD y DTS Express) y TrueHD/MLP.
# - libffmpegJNI.so: el puente enlazado a esas bibliotecas.
# - El AAR con las clases de Media3 (FfmpegAudioRenderer…), compilado con el mismo AGP de la app
#   (tools/ffmpeg/aar).
# Todo con el NDK fijo, para arm64-v8a, armeabi-v7a y x86_64, con las librerías alineadas a páginas
# de 16 KB, sin símbolos y desde una carpeta fija (MAUG_FFMPEG_WORK, /tmp/maug-ffmpeg; el script
# solo borra una carpeta que creó él): así otra corrida con las mismas versiones da los mismos bytes
# y el mismo SHA-256.
#
# Para cambiar de versión (otra de Media3 o de FFmpeg) cambia las de abajo con su commit, súbelo a
# una rama mejoras/* y el flujo compila y guarda el AAR nuevo en la rama. El AAR tiene que ser de la
# misma versión de Media3 que la app (app/build.gradle no compila con otra).
set -Eeuo pipefail

# ---- Versiones fijas --------------------------------------------------------------------------
MEDIA3_VERSION="1.11.1"
MEDIA3_COMMIT="8c6678b657ede1e7883fc164ef73ed483c7796c3"   # etiqueta 1.11.1 de androidx/media
FFMPEG_VERSION="8.1.3"
FFMPEG_COMMIT="1041abdc962f4cc4f394aa8de9dc5236c0c3b9e7"   # etiqueta n8.1.3 de FFmpeg
NDK_VERSION="29.0.14206865"                                # NDK r29
API=24                                                     # minSdk de la app
ABIS=(arm64-v8a armeabi-v7a x86_64)
DECODERS=(ac3 eac3 dca truehd mlp)

FFMPEG_GIT="https://github.com/FFmpeg/FFmpeg.git"          # espejo oficial de git.ffmpeg.org
MEDIA3_GIT="https://github.com/androidx/media.git"
FFMPEG_TARBALL="ffmpeg-$FFMPEG_VERSION.tar.xz"

# Dónde publica MaugPelis el código fuente (la LGPL pide ofrecerlo desde un lugar que cualquiera
# pueda bajar): la página (sin barra al final; desde la app con ?desde=app, que no muestra enlaces
# a comprar: política de pagos de Play) y, sueltos y sin menú, ffmpeg-<versión>.tar.xz, build.sh,
# FFmpeg.txt, ffmpeg_jni.cc y LICENSE-APACHE-2.0.txt (bash tools/ffmpeg/build.sh fuente los arma).
SOURCE_PAGE="https://maugpelis.com/codigo-abierto"
SOURCE_PAGE_APP="$SOURCE_PAGE?desde=app"
SOURCE_BASE="$SOURCE_PAGE/ffmpeg"
APACHE_FILE="LICENSE-APACHE-2.0.txt"

REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TOOLS="$REPO/tools/ffmpeg"
WORK="${MAUG_FFMPEG_WORK:-/tmp/maug-ffmpeg}"
WORK_MARKER=".carpeta-de-tools-ffmpeg"
OUT="$TOOLS/build"
AAR_NAME="media3-decoder-ffmpeg-$MEDIA3_VERSION.aar"
LICENSES_DIR="$REPO/app/src/main/assets/licencias"
OPEN_SOURCE_DIR="$TOOLS/codigo-abierto"
FFMPEG_LIBS=(libavutil.so libswresample.so libavcodec.so)

case "$(uname -s)" in
    Linux) HOST_TAG="linux-x86_64" ;;
    Darwin) HOST_TAG="darwin-x86_64" ;;
    *) echo "Sistema no soportado: $(uname -s)" >&2; exit 1 ;;
esac

# ---- Avisos para GitHub Actions ---------------------------------------------------------------
# El registro del CI no se puede leer desde las sesiones en la nube: lo importante sale como
# anotación del commit (::notice / ::error).
in_ci() { [ -n "${GITHUB_ACTIONS:-}" ]; }

annotation_escape() {
    local text="$1"
    text="${text//'%'/'%25'}"
    text="${text//$'\r'/'%0D'}"
    text="${text//$'\n'/'%0A'}"
    printf '%s' "$text"
}

notice() {
    if in_ci; then
        printf '::notice title=%s::%s\n' "$1" "$(annotation_escape "$2")"
    else
        printf '%s: %s\n' "$1" "$2"
    fi
}

fail() {
    if in_ci; then
        printf '::error title=%s::%s\n' "$1" "$(annotation_escape "$2")"
    fi
    printf 'ERROR (%s): %s\n' "$1" "$2" >&2
    exit 1
}

# Corre un paso con su salida en un archivo; si falla, muestra el final (y, en configure, el de
# ffbuild/config.log, que dice qué prueba falló; en Gradle, los errores) como error del commit.
run_logged() {
    local title="$1" log="$2"
    shift 2
    mkdir -p "$(dirname "$log")"
    if ! "$@" > "$log" 2>&1; then
        local tail_text extra=""
        tail_text="$(tail -n 60 "$log")"
        case "$title" in
            configure*)
                if [ -f ffbuild/config.log ]; then
                    extra=$'\n--- ffbuild/config.log ---\n'"$(tail -n 30 ffbuild/config.log)"
                fi
                ;;
            AAR* | Prueba*)
                # Los errores de javac, de Gradle y de las pruebas quedan arriba de la cola.
                extra=$'\n--- errores ---\n'"$(grep -E 'error:|warning: \[|What went wrong|^> |FAILED|Exception|expected|Assertion' "$log" | head -n 40)"
                ;;
        esac
        fail "$title" "$tail_text$extra"
    fi
}

sha256_of() {
    if command -v sha256sum > /dev/null; then
        sha256sum "$1" | cut -d ' ' -f 1
    else
        shasum -a 256 "$1" | cut -d ' ' -f 1
    fi
}

# ---- Opciones de configure ----------------------------------------------------------------------
# Las mismas que la extensión de Media3 (build_ffmpeg.sh), pero en bibliotecas compartidas, sin
# detectar nada del sistema y con pthreads (las tablas de los decodificadores se arman una sola vez
# aunque haya varios reproductores a la vez).
common_flags() {
    # shellcheck disable=SC2054  # las comas son de -Wl,…, no separan elementos
    COMMON_FLAGS=(
        --target-os=android
        --enable-cross-compile
        --disable-static
        --enable-shared
        --enable-pic
        --disable-autodetect
        --enable-pthreads
        --disable-programs
        --disable-doc
        --disable-everything
        --disable-avdevice
        --disable-avformat
        --disable-swscale
        --disable-avfilter
        --disable-network
        --enable-swresample
        --disable-stripping
        --extra-ldflags=-Wl,-z,max-page-size=16384
    )
    local decoder
    for decoder in "${DECODERS[@]}"; do
        COMMON_FLAGS+=("--enable-decoder=$decoder")
    done
}

# El puente de Media3 (ffmpeg_jni.cc): como su CMakeLists (visibilidad oculta, libc++ estática),
# enlazado a las bibliotecas compartidas y alineado a 16 KB.
JNI_CFLAGS=(-std=c++11 -O2 -g -fPIC -fvisibility=hidden -fvisibility-inlines-hidden)
# shellcheck disable=SC2054  # las comas son de -Wl,…
JNI_LDFLAGS=(-lswresample -lavcodec -lavutil -llog -static-libstdc++
    -Wl,-soname,libffmpegJNI.so -Wl,--no-undefined -Wl,--as-needed -Wl,--gc-sections
    -Wl,-z,relro -Wl,-z,now -Wl,-z,max-page-size=16384 -Wl,--build-id=sha1)

# Triple del compilador y opciones propias de cada arquitectura ($1: carpeta de las herramientas).
abi_flags() {
    local abi="$1" bin="$2"
    case "$abi" in
        arm64-v8a)
            TRIPLE="aarch64-linux-android"
            ABI_FLAGS=(--arch=aarch64 --cpu=armv8-a)
            ;;
        armeabi-v7a)
            TRIPLE="armv7a-linux-androideabi"
            # shellcheck disable=SC2054  # las comas son de -Wl,…
            ABI_FLAGS=(--arch=arm --cpu=armv7-a
                "--extra-cflags=-march=armv7-a -mfloat-abi=softfp"
                --extra-ldflags=-Wl,--fix-cortex-a8)
            ;;
        x86_64)
            TRIPLE="x86_64-linux-android"
            # Sin ensamblador, como Media3 (necesitaría nasm); solo emuladores y Chromebooks.
            ABI_FLAGS=(--arch=x86_64 --cpu=x86-64 --disable-asm)
            ;;
        *) fail "Arquitectura" "No sé compilar para $abi" ;;
    esac
    ABI_FLAGS+=(
        "--cross-prefix=$bin/$TRIPLE$API-"
        "--cxx=$bin/$TRIPLE$API-clang++"
        "--nm=$bin/llvm-nm"
        "--ar=$bin/llvm-ar"
        "--ranlib=$bin/llvm-ranlib"
        "--strip=$bin/llvm-strip"
    )
}

# ---- Textos de licencias (assets/licencias y el código fuente) ---------------------------------
write_licenses() {
    local ffmpeg_src="$1" media3_src="$2" dest="$3"
    [ -f "$ffmpeg_src/COPYING.LGPLv2.1" ] || fail "Licencias" "No está COPYING.LGPLv2.1 en $ffmpeg_src"
    [ -f "$media3_src/LICENSE" ] || fail "Licencias" "No está LICENSE (Apache 2.0) en $media3_src"
    grep -q "Apache License" "$media3_src/LICENSE" || fail "Licencias" "$media3_src/LICENSE no es la Apache 2.0"
    mkdir -p "$dest"
    cp "$ffmpeg_src/COPYING.LGPLv2.1" "$dest/LGPL-2.1.txt"
    cp "$media3_src/LICENSE" "$dest/$APACHE_FILE"
    common_flags
    {
        cat <<EOF
FFmpeg $FFMPEG_VERSION en MaugPelis (audios AC-3, E-AC-3, DTS y TrueHD)

Copyright (c) 2000-2026 the FFmpeg developers

Este programa usa bibliotecas del proyecto FFmpeg con la licencia GNU LGPL versión 2.1 o
posterior. El texto completo de la licencia está en LGPL-2.1.txt, junto a este archivo en la app
(en el código fuente de FFmpeg es COPYING.LGPLv2.1).

This software uses libraries from the FFmpeg project under the LGPLv2.1.

FFmpeg es de sus autores (https://ffmpeg.org/). MaugPelis no lo modificó: usa FFmpeg
$FFMPEG_VERSION tal como lo publicó FFmpeg, sin parches, compilado con las opciones de abajo.

Para qué se usa:
Para oír los audios AC-3, E-AC-3, DTS y TrueHD en los teléfonos que no traen esos decodificadores.
Solo se compilaron esos decodificadores (${DECODERS[*]}) y las bibliotecas libavcodec, libavutil y
libswresample, como bibliotecas compartidas: libavcodec.so, libavutil.so y libswresample.so, para
arm64-v8a, armeabi-v7a y x86_64. Ninguna parte con licencia GPL ni "nonfree".

Código fuente:
Todo lo que hace falta para volver a compilar estas bibliotecas está en
$SOURCE_PAGE_APP

El código de FFmpeg es el archivo $SOURCE_BASE/$FFMPEG_TARBALL:
FFmpeg $FFMPEG_VERSION tal cual, el mismo árbol que se compiló (la etiqueta n$FFMPEG_VERSION, commit
$FFMPEG_COMMIT). Para rehacerlo desde un clon de
https://github.com/FFmpeg/FFmpeg (o de git.ffmpeg.org):
  git archive --format=tar --prefix=ffmpeg-$FFMPEG_VERSION/ $FFMPEG_COMMIT | xz -9 > $FFMPEG_TARBALL

En la misma página están también build.sh, el script con el que se compiló (configure, make, el
puente y las comprobaciones); ffmpeg_jni.cc, el puente entre la app y FFmpeg (libffmpegJNI.so), de
la extensión de FFmpeg de Media3 $MEDIA3_VERSION (commit $MEDIA3_COMMIT de
https://github.com/androidx/media), con licencia Apache 2.0; el texto de esa licencia
($APACHE_FILE, también junto a este archivo en la app), y este aviso.

Si no puedes bajarlo, pídelo por el soporte de la app (Ajustes > Ayuda y acerca de > Soporte) en
los próximos tres años y te mandamos una copia.

Cómo se compiló:
NDK de Android $NDK_VERSION, API $API. Opciones de configure comunes:
EOF
        local flag
        for flag in "${COMMON_FLAGS[@]}"; do
            printf '  %s\n' "$flag"
        done
        printf '\n%s\n' "Además, según la arquitectura (<NDK> es la carpeta del NDK; <bin> es <NDK>/toolchains/llvm/prebuilt/$HOST_TAG/bin):"
        local abi
        for abi in "${ABIS[@]}"; do
            abi_flags "$abi" "<bin>"
            printf '  %s:' "$abi"
            for flag in "${ABI_FLAGS[@]}"; do
                case "$flag" in
                    *" "*) printf ' "%s"' "$flag" ;;
                    *) printf ' %s' "$flag" ;;
                esac
            done
            printf '\n'
        done
        printf '\n%s\n' "libffmpegJNI.so se compila con <bin>/<triple>$API-clang++ del mismo NDK:"
        printf '  %s -I<FFmpeg>/include -c ffmpeg_jni.cc\n' "${JNI_CFLAGS[*]}"
        printf '  -shared -o libffmpegJNI.so ffmpeg_jni.o -L<FFmpeg>/lib %s\n' "${JNI_LDFLAGS[*]}"
        cat <<EOF

Cómo cambiar FFmpeg por otra versión:
libffmpegJNI.so carga libavcodec.so, libavutil.so y libswresample.so por su nombre, como
bibliotecas aparte: se pueden cambiar por otra versión de FFmpeg compatible, compilada con estas
opciones o con otras. Para eso hay que volver a armar el APK con las bibliotecas nuevas y firmarlo
con otra clave. Si el servidor de MaugPelis tiene encendida la comprobación de la firma de la app,
una app firmada con otra clave no pasa de la pantalla de inicio: eso lo decide el servidor, no estas
bibliotecas.
EOF
    } > "$dest/FFmpeg.txt"
}

# ---- Carpeta de trabajo ---------------------------------------------------------------------------
# Siempre la misma ruta (queda dentro de las librerías: configure y la huella de lld). Se borra
# entera en cada corrida, así que solo se borra una carpeta que haya creado este script.
prepare_work() {
    case "$WORK" in
        /?*) ;;
        *) fail "Carpeta de trabajo" "MAUG_FFMPEG_WORK tiene que ser una ruta absoluta y no la raíz: $WORK" ;;
    esac
    if [ -e "$WORK" ] || [ -L "$WORK" ]; then
        [ -f "$WORK/$WORK_MARKER" ] ||
            fail "Carpeta de trabajo" "$WORK ya existe y no la creó este script (no tiene $WORK_MARKER): no la borro. Usa otra ruta en MAUG_FFMPEG_WORK."
        rm -rf "$WORK"
    fi
    mkdir -p "$WORK/logs"
    printf '%s\n' "Carpeta de trabajo de tools/ffmpeg/build.sh: se borra entera en cada corrida." \
        > "$WORK/$WORK_MARKER"
}

# ---- NDK --------------------------------------------------------------------------------------
resolve_ndk() {
    local sdk="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
    [ -n "$sdk" ] && [ -d "$sdk" ] || fail "SDK de Android" "Define ANDROID_HOME con la carpeta del SDK."
    local ndk="$sdk/ndk/$NDK_VERSION"
    if [ ! -f "$ndk/source.properties" ]; then
        local sdkmanager="$sdk/cmdline-tools/latest/bin/sdkmanager"
        [ -x "$sdkmanager" ] || sdkmanager="$(command -v sdkmanager || true)"
        [ -n "$sdkmanager" ] || fail "NDK" "Falta el NDK $NDK_VERSION y no encuentro sdkmanager para instalarlo."
        run_logged "Instalar el NDK $NDK_VERSION" "$WORK/logs/ndk.log" "$sdkmanager" --install "ndk;$NDK_VERSION"
    fi
    grep -q "^Pkg.Revision *= *$NDK_VERSION\$" "$ndk/source.properties" ||
        fail "NDK" "$ndk no es el NDK $NDK_VERSION."
    ln -s "$ndk" "$WORK/ndk"
    BIN="$WORK/ndk/toolchains/llvm/prebuilt/$HOST_TAG/bin"
    [ -x "$BIN/clang" ] || fail "NDK" "No encuentro clang en $BIN"
    READELF="$BIN/llvm-readelf"
    NM="$BIN/llvm-nm"
    STRIP="$BIN/llvm-strip"
}

# ---- Código fuente ------------------------------------------------------------------------------
# FFmpeg: la etiqueta del espejo oficial en GitHub, comprobada contra el commit fijado. Se compila
# el árbol de git archive de ese commit (lo mismo que se publica), en $WORK/ffmpeg.
fetch_ffmpeg_source() {
    run_logged "Bajar ffmpeg" "$WORK/logs/clone-ffmpeg.log" \
        git -c advice.detachedHead=false clone --quiet --depth 1 --branch "n$FFMPEG_VERSION" \
        "$FFMPEG_GIT" "$WORK/ffmpeg-git"
    local head
    head="$(git -C "$WORK/ffmpeg-git" rev-parse HEAD)"
    [ "$head" = "$FFMPEG_COMMIT" ] ||
        fail "Bajar ffmpeg" "La etiqueta n$FFMPEG_VERSION apunta a $head y no a $FFMPEG_COMMIT."
    mkdir -p "$WORK/ffmpeg"
    # Sin .git: la versión sale de RELEASE ("8.1.3"), como al compilar el archivo publicado.
    run_logged "Abrir ffmpeg" "$WORK/logs/archive-ffmpeg.log" extract_ffmpeg_tree "$WORK/ffmpeg"
    [ "$(tr -d '[:space:]' < "$WORK/ffmpeg/RELEASE")" = "$FFMPEG_VERSION" ] ||
        fail "Bajar ffmpeg" "El árbol no es FFmpeg $FFMPEG_VERSION (RELEASE dice $(cat "$WORK/ffmpeg/RELEASE"))."
}

# El árbol de git archive del commit fijado (sin el prefijo ffmpeg-<versión>/) en la carpeta $1.
extract_ffmpeg_tree() {
    git -C "$WORK/ffmpeg-git" archive --format=tar --prefix="ffmpeg-$FFMPEG_VERSION/" "$FFMPEG_COMMIT" |
        tar -x -C "$1" --strip-components=1
}

# El mismo árbol, como lo publica la web: ffmpeg-<versión>.tar.xz en la carpeta $1.
pack_ffmpeg_tarball() {
    git -C "$WORK/ffmpeg-git" archive --format=tar --prefix="ffmpeg-$FFMPEG_VERSION/" "$FFMPEG_COMMIT" |
        xz -9 > "$1/$FFMPEG_TARBALL"
}

# Media3: solo libraries/decoder_ffmpeg (y LICENSE), en el commit fijado. Queda en $WORK/media.
clone_media3() {
    run_logged "Bajar media3" "$WORK/logs/clone-media3.log" \
        git -c advice.detachedHead=false clone --quiet --depth 1 --branch "$MEDIA3_VERSION" \
        --filter=blob:none --sparse "$MEDIA3_GIT" "$WORK/media"
    local head
    head="$(git -C "$WORK/media" rev-parse HEAD)"
    [ "$head" = "$MEDIA3_COMMIT" ] || fail "Bajar media3" "La etiqueta $MEDIA3_VERSION apunta a $head y no a $MEDIA3_COMMIT."
    run_logged "Bajar media3" "$WORK/logs/sparse-media3.log" \
        git -C "$WORK/media" sparse-checkout set libraries/decoder_ffmpeg
    MEDIA3_MODULE="$WORK/media/libraries/decoder_ffmpeg"
    JNI_SRC="$MEDIA3_MODULE/src/main/jni/ffmpeg_jni.cc"
    [ -f "$JNI_SRC" ] || fail "Bajar media3" "No está $JNI_SRC"
}

# Los archivos sueltos de https://maugpelis.com/codigo-abierto/ffmpeg/, en $OUT/codigo-abierto/ffmpeg/.
write_open_source_folder() {
    local dest="$OUT/codigo-abierto/ffmpeg"
    rm -rf "$dest"
    mkdir -p "$dest"
    command -v xz > /dev/null || fail "Código fuente" "Falta xz para armar $FFMPEG_TARBALL."
    run_logged "Armar $FFMPEG_TARBALL" "$WORK/logs/tarball.log" pack_ffmpeg_tarball "$dest"
    cp "$TOOLS/build.sh" "$dest/build.sh"
    cp "$OUT/licencias/FFmpeg.txt" "$dest/FFmpeg.txt"
    cp "$JNI_SRC" "$dest/ffmpeg_jni.cc"
    cp "$OUT/licencias/$APACHE_FILE" "$dest/$APACHE_FILE"
    notice "Código fuente para la web" "En $SOURCE_BASE/ van sueltos (la página es $SOURCE_PAGE): $FFMPEG_TARBALL (git archive del commit $FFMPEG_COMMIT), build.sh, FFmpeg.txt, ffmpeg_jni.cc y $APACHE_FILE."
}

# ---- Comprobaciones de cada librería ---------------------------------------------------------------
check_config() {
    local abi="$1" config_h="$WORK/ffmpeg/config.h" components_h="$WORK/ffmpeg/config_components.h"
    grep -q '^#define FFMPEG_LICENSE "LGPL version 2.1 or later"$' "$config_h" ||
        fail "Licencia ($abi)" "FFmpeg no quedó como LGPL 2.1 o posterior: $(grep FFMPEG_LICENSE "$config_h")"
    local option
    for option in CONFIG_GPL CONFIG_VERSION3 CONFIG_NONFREE; do
        grep -q "^#define $option 0\$" "$config_h" || fail "Licencia ($abi)" "$option está encendido."
    done
    local decoder upper
    for decoder in "${DECODERS[@]}"; do
        upper="$(printf '%s' "$decoder" | tr '[:lower:]' '[:upper:]')"
        grep -q "^#define CONFIG_${upper}_DECODER 1\$" "$components_h" ||
            fail "Decodificadores ($abi)" "Falta el decodificador $decoder."
    done
    local enabled
    enabled="$(grep -c '^#define CONFIG_[A-Z0-9_]*_DECODER 1$' "$components_h" || true)"
    [ "$enabled" -eq "${#DECODERS[@]}" ] ||
        fail "Decodificadores ($abi)" "Hay $enabled decodificadores y se pidieron ${#DECODERS[@]}: $(grep '_DECODER 1$' "$components_h" | tr '\n' ' ')"
}

check_elf() {
    local abi="$1" lib="$2" name
    name="$(basename "$lib")"
    # Segmentos LOAD alineados a 16 KB (Google Play, Android 15+).
    local align
    for align in $("$READELF" -lW "$lib" | awk '$1 == "LOAD" { print $NF }'); do
        [ $((align)) -ge 16384 ] || fail "16 KB ($abi)" "$name tiene un segmento alineado a $align."
    done
    # Sin relocalizaciones de texto (Android 6+ no carga esas librerías).
    local dynamic
    dynamic="$("$READELF" -dW "$lib")"
    if grep -q TEXTREL <<< "$dynamic"; then
        fail "Relocalizaciones ($abi)" "$name tiene TEXTREL."
    fi
    # El nombre con el que la busca el enlazador es el del archivo (sin .so.62).
    local soname
    soname="$(sed -n 's/.*(SONAME).*\[\(.*\)\].*/\1/p' <<< "$dynamic")"
    [ "$soname" = "$name" ] || fail "SONAME ($abi)" "$name se llama «$soname» por dentro."
}

# ---- Compilar ---------------------------------------------------------------------------------
build_all() {
    local install="$1"
    prepare_work
    resolve_ndk
    fetch_ffmpeg_source
    clone_media3
    common_flags
    local jni_libs="$WORK/jniLibs" symbols="$WORK/simbolos"
    mkdir -p "$jni_libs" "$symbols"
    local jobs abi prefix jni_build lib needed exported
    jobs="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)"

    for abi in "${ABIS[@]}"; do
        abi_flags "$abi" "$BIN"
        echo "== FFmpeg $FFMPEG_VERSION para $abi"
        cd "$WORK/ffmpeg"
        prefix="$WORK/instalado/$abi"
        run_logged "configure ($abi)" "$WORK/logs/configure-$abi.log" \
            ./configure --prefix="$prefix" "${COMMON_FLAGS[@]}" "${ABI_FLAGS[@]}"
        check_config "$abi"
        run_logged "make ($abi)" "$WORK/logs/make-$abi.log" make -j"$jobs"
        run_logged "make install ($abi)" "$WORK/logs/install-$abi.log" make install-libs install-headers
        run_logged "make clean ($abi)" "$WORK/logs/clean-$abi.log" make distclean

        echo "== libffmpegJNI.so para $abi"
        jni_build="$WORK/jni/$abi"
        mkdir -p "$jni_build"
        cd "$jni_build"
        run_logged "ffmpeg_jni.cc ($abi)" "$WORK/logs/jni-compile-$abi.log" \
            "$BIN/$TRIPLE$API-clang++" "${JNI_CFLAGS[@]}" -I"$prefix/include" -c "$JNI_SRC" -o ffmpeg_jni.o
        run_logged "libffmpegJNI.so ($abi)" "$WORK/logs/jni-link-$abi.log" \
            "$BIN/$TRIPLE$API-clang++" -shared -o libffmpegJNI.so ffmpeg_jni.o -L"$prefix/lib" "${JNI_LDFLAGS[@]}"

        mkdir -p "$jni_libs/$abi" "$symbols/$abi"
        for lib in "${FFMPEG_LIBS[@]}"; do
            cp "$prefix/lib/$lib" "$symbols/$abi/$lib"
        done
        cp "$jni_build/libffmpegJNI.so" "$symbols/$abi/libffmpegJNI.so"
        for lib in "${FFMPEG_LIBS[@]}" libffmpegJNI.so; do
            "$STRIP" --strip-unneeded -o "$jni_libs/$abi/$lib" "$symbols/$abi/$lib"
            check_elf "$abi" "$jni_libs/$abi/$lib"
        done

        # El puente pide FFmpeg por su nombre (y no trae libc++_shared.so, que chocaría con otras).
        needed="$("$READELF" -dW "$jni_libs/$abi/libffmpegJNI.so" | sed -n 's/.*(NEEDED).*\[\(.*\)\].*/\1/p' | sort | tr '\n' ' ')"
        for lib in "${FFMPEG_LIBS[@]}"; do
            case " $needed " in
                *" $lib "*) ;;
                *) fail "libffmpegJNI.so ($abi)" "No pide $lib; pide: $needed" ;;
            esac
        done
        case " $needed " in
            *"c++_shared"*) fail "libffmpegJNI.so ($abi)" "Pide libc++_shared.so: $needed" ;;
        esac
        exported="$("$NM" -D --defined-only "$jni_libs/$abi/libffmpegJNI.so")"
        grep -qw JNI_OnLoad <<< "$exported" || fail "libffmpegJNI.so ($abi)" "No exporta JNI_OnLoad."
    done

    echo "== AAR (Media3 $MEDIA3_VERSION)"
    rm -rf "$TOOLS/aar/build"
    run_logged "AAR (Gradle)" "$WORK/logs/gradle.log" \
        sh "$REPO/gradlew" -p "$TOOLS/aar" --no-daemon \
        -Pmaug.media3Version="$MEDIA3_VERSION" \
        -Pmaug.media3Module="$MEDIA3_MODULE" \
        -Pmaug.jniLibs="$jni_libs" \
        -Pmaug.minSdk="$API" \
        assembleRelease
    local built_aar
    built_aar="$(find "$TOOLS/aar/build/outputs/aar" -name '*-release.aar' | head -n 1)"
    [ -n "$built_aar" ] || fail "AAR (Gradle)" "Gradle no dejó el AAR en tools/ffmpeg/aar/build/outputs/aar."

    rm -rf "$OUT"
    mkdir -p "$OUT"
    python3 -I "$TOOLS/aar_tool.py" normalize "$built_aar" "$OUT/$AAR_NAME"
    python3 -I "$TOOLS/aar_tool.py" check "$OUT/$AAR_NAME" "$jni_libs" "${ABIS[@]}"
    python3 -I "$TOOLS/aar_tool.py" verify "$OUT/$AAR_NAME" "${ABIS[@]}"
    printf '%s  %s\n' "$(sha256_of "$OUT/$AAR_NAME")" "$AAR_NAME" > "$OUT/$AAR_NAME.sha256"
    write_licenses "$WORK/ffmpeg" "$WORK/media" "$OUT/licencias"
    python3 -I "$TOOLS/aar_tool.py" licenses "$OUT/licencias" "$FFMPEG_VERSION" "$FFMPEG_COMMIT"
    python3 -I "$TOOLS/aar_tool.py" zipdir "$symbols" "$OUT/simbolos-ffmpeg-$FFMPEG_VERSION.zip"

    local sha sizes
    sha="$(cut -d ' ' -f 1 "$OUT/$AAR_NAME.sha256")"
    sizes="$(python3 -I "$TOOLS/aar_tool.py" sizes "$OUT/$AAR_NAME")"
    notice "AAR de FFmpeg" "$AAR_NAME (FFmpeg $FFMPEG_VERSION, NDK $NDK_VERSION)
SHA-256 $sha
$sizes"

    if [ "$install" -eq 1 ]; then
        find "$REPO/app/libs" -maxdepth 1 -name 'media3-decoder-ffmpeg-*' -delete
        cp "$OUT/$AAR_NAME" "$OUT/$AAR_NAME.sha256" "$REPO/app/libs/"
        mkdir -p "$LICENSES_DIR" "$OPEN_SOURCE_DIR"
        cp "$OUT/licencias/FFmpeg.txt" "$OUT/licencias/LGPL-2.1.txt" "$OUT/licencias/$APACHE_FILE" "$LICENSES_DIR/"
        cp "$JNI_SRC" "$OPEN_SOURCE_DIR/ffmpeg_jni.cc"
        echo "Copiado a app/libs/$AAR_NAME, app/src/main/assets/licencias/ y tools/ffmpeg/codigo-abierto/."
    fi
    echo "Listo: $OUT/$AAR_NAME"
}

# ---- Prueba en un emulador -----------------------------------------------------------------------
# La prueba instrumentada del AAR (tools/ffmpeg/aar/src/androidTest) en un emulador Android 14
# x86_64 sin ventana (imagen ATD de AOSP): que las librerías carguen y abran los decodificadores.
# Después de compilar; en Linux con KVM. El emulador lo arranca este script (con Gradle Managed
# Devices no arrancaba en el CI y no decía por qué): su salida queda en $WORK/logs y, si falla,
# sale en el error.
EMULATOR_IMAGE="system-images;android-34;aosp_atd;x86_64"
EMULATOR_AVD="maug-ffmpeg-api34"
EMULATOR_PORT=5554
EMULATOR_SERIAL="emulator-$EMULATOR_PORT"
EMULATOR_PID=""
ADB=""

stop_emulator() {
    if [ -n "$EMULATOR_PID" ]; then
        "$ADB" -s "$EMULATOR_SERIAL" emu kill > /dev/null 2>&1 || true
        local waited=0
        while kill -0 "$EMULATOR_PID" 2> /dev/null && [ "$waited" -lt 30 ]; do
            sleep 1
            waited=$((waited + 1))
        done
        kill "$EMULATOR_PID" 2> /dev/null || true
        EMULATOR_PID=""
    fi
}

emulator_failure() {
    stop_emulator
    fail "Prueba en el emulador" "$1"$'\n--- emulador ---\n'"$(tail -n 40 "$WORK/logs/emulador-arranque.log" 2> /dev/null || true)"
}

create_avd() {
    echo no | "$1" create avd --force --name "$EMULATOR_AVD" --package "$EMULATOR_IMAGE" --device pixel_6
    # La prueba instala un APK chico: 2 GB de datos alcanzan (el de fábrica pide más de 7 GB de
    # disco y en el CI no entraba: «Not enough space to create userdata partition»).
    local config="$ANDROID_AVD_HOME/$EMULATOR_AVD.avd/config.ini"
    [ -f "$config" ] || return 1
    sed -i.bak '/^disk\.dataPartition\.size=/d' "$config"
    rm -f "$config.bak"
    printf '%s\n' "disk.dataPartition.size=2G" >> "$config"
}

# Espera a que el emulador arranque y a que su gestor de paquetes responda (hasta 10 minutos).
wait_for_emulator() {
    local waited=0 booted=""
    while [ "$waited" -lt 600 ]; do
        kill -0 "$EMULATOR_PID" 2> /dev/null || emulator_failure "El emulador se cerró al arrancar."
        booted="$("$ADB" -s "$EMULATOR_SERIAL" shell getprop sys.boot_completed 2> /dev/null | tr -d '\r' || true)"
        if [ "$booted" = "1" ] && "$ADB" -s "$EMULATOR_SERIAL" shell pm path android > /dev/null 2>&1; then
            return 0
        fi
        sleep 5
        waited=$((waited + 5))
    done
    emulator_failure "El emulador no terminó de arrancar en 10 minutos."
}

run_device_tests() {
    [ -f "$WORK/$WORK_MARKER" ] && [ -d "$WORK/jniLibs" ] && [ -d "$WORK/media/libraries/decoder_ffmpeg" ] ||
        fail "Prueba en el emulador" "Primero compila (bash tools/ffmpeg/build.sh): no está $WORK/jniLibs."
    local sdk="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
    [ -n "$sdk" ] && [ -d "$sdk" ] || fail "Prueba en el emulador" "Define ANDROID_HOME con la carpeta del SDK."
    local tools="$sdk/cmdline-tools/latest/bin"
    [ -x "$tools/sdkmanager" ] && [ -x "$tools/avdmanager" ] ||
        fail "Prueba en el emulador" "No encuentro sdkmanager y avdmanager en $tools."
    [ -w /dev/kvm ] || fail "Prueba en el emulador" "Sin acceso a /dev/kvm: el emulador x86_64 necesita KVM."

    run_logged "Instalar el emulador" "$WORK/logs/emulador-sdk.log" \
        "$tools/sdkmanager" --install emulator platform-tools "$EMULATOR_IMAGE"
    ADB="$sdk/platform-tools/adb"
    export ANDROID_AVD_HOME="$WORK/avd"
    mkdir -p "$ANDROID_AVD_HOME"
    run_logged "Crear el emulador" "$WORK/logs/emulador-avd.log" create_avd "$tools/avdmanager"

    "$ADB" start-server > /dev/null 2>&1 || true
    "$sdk/emulator/emulator" -avd "$EMULATOR_AVD" -port "$EMULATOR_PORT" -no-window -no-audio \
        -no-boot-anim -no-snapshot -gpu swiftshader_indirect -accel on \
        > "$WORK/logs/emulador-arranque.log" 2>&1 &
    EMULATOR_PID=$!
    trap stop_emulator EXIT
    wait_for_emulator

    export ANDROID_SERIAL="$EMULATOR_SERIAL"
    rm -rf "$TOOLS/aar/build/outputs/androidTest-results"
    run_logged "Prueba en el emulador" "$WORK/logs/emulador.log" \
        sh "$REPO/gradlew" -p "$TOOLS/aar" --no-daemon \
        -Pmaug.media3Version="$MEDIA3_VERSION" \
        -Pmaug.media3Module="$WORK/media/libraries/decoder_ffmpeg" \
        -Pmaug.jniLibs="$WORK/jniLibs" \
        -Pmaug.minSdk="$API" \
        connectedDebugAndroidTest
    stop_emulator
    rm -rf "$ANDROID_AVD_HOME"

    local tests
    tests="$(cat "$TOOLS"/aar/build/outputs/androidTest-results/connected/debug/*.xml 2> /dev/null | grep -o '<testcase ' | wc -l | tr -d ' ')"
    [ "${tests:-0}" -gt 0 ] || fail "Prueba en el emulador" "Gradle no dejó resultados de las pruebas en tools/ffmpeg/aar/build."
    notice "FFmpeg en el emulador" "Android 14 x86_64: $tests pruebas bien; las librerías cargan y abren los decodificadores ${DECODERS[*]}."
}

# ---- Qué hacer -----------------------------------------------------------------------------------
case "${1:-}" in
    licencias)
        [ -n "${2:-}" ] && [ -n "${3:-}" ] ||
            fail "Licencias" "Uso: build.sh licencias <código de FFmpeg> <código de androidx/media>"
        write_licenses "$2" "$3" "$LICENSES_DIR"
        echo "Listo: $LICENSES_DIR/FFmpeg.txt, LGPL-2.1.txt y $APACHE_FILE"
        ;;
    fuente)
        prepare_work
        fetch_ffmpeg_source
        clone_media3
        rm -rf "$OUT"
        mkdir -p "$OUT"
        write_licenses "$WORK/ffmpeg" "$WORK/media" "$OUT/licencias"
        write_open_source_folder
        echo "Listo: $OUT/codigo-abierto/ffmpeg"
        ;;
    probar)
        run_device_tests
        ;;
    --instalar)
        build_all 1
        ;;
    "")
        build_all 0
        ;;
    *)
        fail "Uso" "Opciones: --instalar, probar, fuente, o licencias <código de FFmpeg> <código de androidx/media>"
        ;;
esac
