video: pre-ship review fixes for the FFmpeg renderer

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.
This commit is contained in:
agra
2026-05-29 07:33:20 +03:00
parent 7243ef7de4
commit c0d55babf3
6 changed files with 206 additions and 36 deletions

View File

@@ -42,20 +42,64 @@ 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"
if [[ ! -d "$MEDIA3_DIR" ]]; then
echo "[ffmpeg-build] cloning Media3 @${MEDIA3_TAG}"
git clone --depth 1 --branch "$MEDIA3_TAG" \
https://github.com/androidx/media.git "$MEDIA3_DIR"
fi
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"
if [[ ! -d "$FFMPEG_DIR" ]]; then
echo "[ffmpeg-build] cloning FFmpeg @${FFMPEG_TAG}"
git clone --depth 1 --branch "$FFMPEG_TAG" \
https://git.ffmpeg.org/ffmpeg.git "$FFMPEG_DIR"
fi
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.