Six prod-blocking issues and three correctness improvements from an
independent code review of 7243ef7. Verified on Huawei Mate 20 (EMUI
11) — playback, rotation, replay-after-end all still work.
- EAGAIN on avcodec_send_packet was silently dropping the input
packet (SimpleDecoder consumed it before we could retry).
ffmpeg_jni.cc now caches a frame drained from the output queue
into pending_frame, retries the send, and the next
ffmpegVideoReceiveFrame emits the cached frame in order before
pulling a new one.
- C.TIME_UNSET == Long.MIN_VALUE == AV_NOPTS_VALUE was an
undocumented coincidence between two upstreams. Gate it
explicitly so a future Media3 sentinel change can't scramble
display-order PTS recovery.
- supportsFormat parses the H.264 profile from format.codecs and
rejects non-8-bit profiles (High 10 / High 4:2:2 / High 4:4:4).
These initialise libavcodec cleanly and only fail at the first
receive — too late for ExoPlayer to fall through to MediaCodec.
Rejecting upfront lets the platform decoder pick them up.
- build_ffmpeg.sh wraps the whole run in a portable mkdir-based
lock and clones into a staging dir + atomic rename with a
sentinel file. Concurrent Gradle daemons no longer corrupt
each other; an interrupted clone leaves no usable state for
the next run to mistake as finished.
- FfmpegOutputSurface and VideoCompositor both used to call
eglTerminate(EGL_DEFAULT_DISPLAY) on teardown. That display is
process-global and shared — the first teardown killed the
other consumer's surface. Drop both calls; per-context cleanup
+ eglReleaseThread is sufficient. Likely cause of any "frozen
surface after second video" report.
- Rotation swap in renderOutputBuffer mutates the public
outputBuffer.width/height. Bound it to SURFACE_YUV output mode
via a currentOutputMode tracker; YUV-mode consumers
(VideoDecoderOutputBufferRenderer.setOutputBuffer) read
width/height expecting CODED dims that match yuvStrides[0] —
the swap would walk chroma off the end of the allocation.
- Fragment shader bumped from mediump to highp. The limited-range
pre-scale (y - 16/255) * (255/219) was at risk of quantizing
through 10-bit mediump and banding dark gradients on older
Mali / Adreno parts. highp on the fragment is universally
supported on GLES2 implementations Android ships post-2014.
- Threading config comment was wrong about what FF_THREAD_SLICE
does for H.264. Replace with the accurate explanation (slice
threading degenerates to single-threaded on iOS's single-slice
encodes; FRAME threading is rejected because of the input-side
latency, not because libavcodec doesn't support it).
- FfmpegVideoDecoder header documents two known limits the
review surfaced but that don't have a clean fix at this layer:
EOS tail-frame loss (~500 ms truncation on first play-through
only; replay is fine because flush_buffers clears libavcodec)
and the size-based colorspace heuristic mislabelling iPhone
6/7-era unspecified-metadata BT.601 1080p clips as BT.709.
152 lines
5.7 KiB
Bash
Executable File
152 lines
5.7 KiB
Bash
Executable File
#!/bin/bash
|
|
# Builds libffmpegJNI.so for all 4 Android ABIs from vendored JNI sources
|
|
# (ffmpeg_jni.cc + CMakeLists.txt in this directory) against upstream
|
|
# Media3 + FFmpeg cloned into WORK_DIR. Driven entirely by environment
|
|
# variables Gradle sets in the buildFfmpegJni task — no user-facing
|
|
# flags. Reruns are cheap once WORK_DIR is populated (Gradle UP-TO-DATE
|
|
# skips the script entirely when JNI sources haven't changed, and even
|
|
# when forced, the FFmpeg static libs are reused if present).
|
|
#
|
|
# Required env:
|
|
# JNI_SRC — this directory (vendored ffmpeg_jni.cc + CMakeLists.txt)
|
|
# NDK_PATH — Android NDK root
|
|
# CMAKE_PATH — directory containing the cmake + ninja binaries
|
|
# OUTPUT_DIR — where to drop the final libffmpegJNI.so per ABI
|
|
# WORK_DIR — scratch directory for upstream clones + intermediate
|
|
# build artefacts (lives under build/ so a clean wipes it)
|
|
#
|
|
# License: FFmpeg is LGPL v2.1. We link to it dynamically (consumers
|
|
# load libffmpegJNI.so at runtime), keeping the LGPL boundary intact and
|
|
# not imposing copyleft on the consuming app. The build below
|
|
# intentionally omits --enable-gpl and --enable-nonfree.
|
|
|
|
set -euo pipefail
|
|
|
|
: "${JNI_SRC:?JNI_SRC env var not set}"
|
|
: "${NDK_PATH:?NDK_PATH env var not set}"
|
|
: "${CMAKE_PATH:?CMAKE_PATH env var not set}"
|
|
: "${OUTPUT_DIR:?OUTPUT_DIR env var not set}"
|
|
: "${WORK_DIR:?WORK_DIR env var not set}"
|
|
|
|
MEDIA3_TAG="${MEDIA3_TAG:-1.9.2}"
|
|
FFMPEG_TAG="${FFMPEG_TAG:-release/6.0}"
|
|
ABIS="${ABIS:-armeabi-v7a arm64-v8a x86 x86_64}"
|
|
MIN_SDK="${MIN_SDK:-21}"
|
|
|
|
case "$(uname -s)" in
|
|
Darwin*) HOST_PLATFORM=darwin-x86_64 ;;
|
|
Linux*) HOST_PLATFORM=linux-x86_64 ;;
|
|
*) echo "Unsupported host: $(uname -s)" >&2; exit 1 ;;
|
|
esac
|
|
|
|
mkdir -p "$WORK_DIR" "$OUTPUT_DIR"
|
|
cd "$WORK_DIR"
|
|
|
|
# Serialise concurrent invocations on the shared WORK_DIR so two
|
|
# Gradle daemons (or two parallel app builds depending on this AAR
|
|
# via the same checkout) can't race on clone / cmake / ninja.
|
|
# `mkdir` is atomic per POSIX — first caller wins. `flock` would be
|
|
# nicer but macOS doesn't ship it. A stale lock from a killed prior
|
|
# run (>30 min old) is broken automatically. The trap clears the
|
|
# lock on normal exit.
|
|
LOCK_DIR="$WORK_DIR/.build-lock"
|
|
if [[ -d "$LOCK_DIR" ]]; then
|
|
if find "$LOCK_DIR" -maxdepth 0 -mmin +30 2>/dev/null | grep -q .; then
|
|
echo "[ffmpeg-build] removing stale lock (>30 min old)"
|
|
rm -rf "$LOCK_DIR"
|
|
fi
|
|
fi
|
|
LOCK_WAIT_SECS=0
|
|
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
|
if [[ "$LOCK_WAIT_SECS" -ge 1800 ]]; then
|
|
echo "[ffmpeg-build] timed out waiting for $LOCK_DIR" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$LOCK_WAIT_SECS" -eq 0 ]]; then
|
|
echo "[ffmpeg-build] another build in progress at $LOCK_DIR, waiting..."
|
|
fi
|
|
sleep 5
|
|
LOCK_WAIT_SECS=$((LOCK_WAIT_SECS + 5))
|
|
done
|
|
trap 'rm -rf "$LOCK_DIR"' EXIT
|
|
|
|
# Sentinel files mark a clone as fully complete so an interrupted
|
|
# clone (network drop, ^C, OOM kill) doesn't leave a half-populated
|
|
# directory the next run mistakes for a finished checkout. Clone
|
|
# into a staging dir, then atomic-rename into place once the
|
|
# sentinel is written.
|
|
clone_if_missing() {
|
|
local target="$1"
|
|
local sentinel="$target/.ux-ffmpeg-build-complete"
|
|
local tag="$2"
|
|
local url="$3"
|
|
local label="$4"
|
|
if [[ -f "$sentinel" ]]; then
|
|
return
|
|
fi
|
|
# Stale partial clone — wipe before re-clone.
|
|
rm -rf "$target" "${target}.staging"
|
|
echo "[ffmpeg-build] cloning $label @${tag}"
|
|
git clone --depth 1 --branch "$tag" "$url" "${target}.staging"
|
|
touch "${target}.staging/.ux-ffmpeg-build-complete"
|
|
mv "${target}.staging" "$target"
|
|
}
|
|
|
|
# 1. Upstream sources — clone once, reuse on subsequent runs.
|
|
MEDIA3_DIR="$WORK_DIR/media3"
|
|
clone_if_missing "$MEDIA3_DIR" "$MEDIA3_TAG" \
|
|
"https://github.com/androidx/media.git" "Media3"
|
|
|
|
FFMPEG_DIR="$MEDIA3_DIR/libraries/decoder_ffmpeg/src/main/jni/ffmpeg"
|
|
clone_if_missing "$FFMPEG_DIR" "$FFMPEG_TAG" \
|
|
"https://git.ffmpeg.org/ffmpeg.git" "FFmpeg"
|
|
|
|
# 2. Drop our extended JNI source + CMake config over the upstream copies
|
|
# so the build produces a video-capable libffmpegJNI.so.
|
|
JNI_BUILD_DIR="$MEDIA3_DIR/libraries/decoder_ffmpeg/src/main/jni"
|
|
cp "$JNI_SRC/ffmpeg_jni.cc" "$JNI_BUILD_DIR/ffmpeg_jni.cc"
|
|
cp "$JNI_SRC/CMakeLists.txt" "$JNI_BUILD_DIR/CMakeLists.txt"
|
|
|
|
# 3. Build FFmpeg static libs per ABI (H.264 decoder only). The
|
|
# sentinel below skips this step if all 4 ABIs already have the
|
|
# static libs from a previous run — important because the FFmpeg
|
|
# static build is the slow part (~30 min for 4 ABIs); subsequent
|
|
# Gradle runs just need to re-link libffmpegJNI.so (~5 sec / ABI).
|
|
MODULE_PATH="$MEDIA3_DIR/libraries/decoder_ffmpeg/src/main"
|
|
NEED_STATIC_BUILD=0
|
|
for ABI in $ABIS; do
|
|
if [[ ! -f "$JNI_BUILD_DIR/ffmpeg/android-libs/$ABI/libavcodec.a" ]]; then
|
|
NEED_STATIC_BUILD=1
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ "$NEED_STATIC_BUILD" -eq 1 ]]; then
|
|
echo "[ffmpeg-build] building FFmpeg static libs (slow on first run)"
|
|
chmod +x "$JNI_BUILD_DIR/build_ffmpeg.sh"
|
|
"$JNI_BUILD_DIR/build_ffmpeg.sh" \
|
|
"$MODULE_PATH" "$NDK_PATH" "$HOST_PLATFORM" "$MIN_SDK" h264
|
|
else
|
|
echo "[ffmpeg-build] FFmpeg static libs already present, reusing"
|
|
fi
|
|
|
|
# 4. Cross-build libffmpegJNI.so per ABI via CMake + Ninja.
|
|
export PATH="$CMAKE_PATH:$PATH"
|
|
for ABI in $ABIS; do
|
|
ABI_BUILD_DIR="$WORK_DIR/jni-out/$ABI"
|
|
mkdir -p "$ABI_BUILD_DIR"
|
|
cmake -G Ninja \
|
|
-DCMAKE_TOOLCHAIN_FILE="$NDK_PATH/build/cmake/android.toolchain.cmake" \
|
|
-DANDROID_ABI="$ABI" \
|
|
-DANDROID_PLATFORM="android-$MIN_SDK" \
|
|
-DCMAKE_BUILD_TYPE=Release \
|
|
-S "$JNI_BUILD_DIR" -B "$ABI_BUILD_DIR" >/dev/null
|
|
ninja -C "$ABI_BUILD_DIR" >/dev/null
|
|
|
|
DEST="$OUTPUT_DIR/$ABI"
|
|
mkdir -p "$DEST"
|
|
cp "$ABI_BUILD_DIR/libffmpegJNI.so" "$DEST/libffmpegJNI.so"
|
|
done
|
|
|
|
echo "[ffmpeg-build] libffmpegJNI.so ready in $OUTPUT_DIR"
|