Adds an LGPL FFmpeg-backed video renderer that slots ahead of Media3's
MediaCodecVideoRenderer via EXTENSION_RENDERER_MODE_PREFER. Resolves
playback failures on Huawei EMUI 11 (Mate 20, Kirin 980): the Codec2
HiSilicon AVC decoder initialises cleanly on iOS High@3.1 streams with
deep DPB + full-range yuvj420p, then errors on the first sample inside
MediaCodecVideoRenderer (init-failure fallback can't catch this).
Google's C2 SW AVC decoder hits its 8-frame output-delay cap on the
same shape and stalls on dequeueOutputBuffer.
Media3's own decoder-ffmpeg ships only an audio renderer;
ExperimentalFfmpegVideoRenderer has been a stub since 2020 (returns
FORMAT_UNSUPPORTED_TYPE, createDecoder returns null). NextLib is
GPL-3.0. So we vendor our own Apache-licensed JNI on top of LGPL
FFmpeg, dynamically linked at runtime.
Build flow:
- android/ffmpeg/ holds the JNI source + CMakeLists + orchestrator
script + LGPL notice. No native binaries in git.
- :ux:buildFfmpegJni Gradle task (wired to preBuild) clones
Media3 1.9.2 + FFmpeg release/6.0 into build/ffmpeg-work/ on
first run, builds h264-only static libs per ABI, links
libffmpegJNI.so per ABI into build/jniLibs/<abi>/. AGP picks
them up via sourceSets.main.jniLibs.srcDirs +=. Gradle
UP-TO-DATE skips the task when ffmpeg_jni.cc / CMakeLists /
build_ffmpeg.sh are unchanged.
Renderer:
- FfmpegVideoDecoder (SimpleDecoder) sends each packet with its
inputBuffer.timeUs as pkt->pts; the JNI overwrites
outputBuffer.timeUs with f->pts on receive so frames emitted in
display order carry their true display PTS (input PTS in decode
order scrambles ExoPlayer's drop logic and halves the render
rate on B-frame streams).
- FfmpegOutputSurface does YUV->RGB in one GLES2 pass against an
EGL window surface sized to display orientation. Y plane uses
GL_NEAREST (1:1 sized, sampling at exact texel centres
preserves luma detail); chroma uses GL_LINEAR. Pre-rotated quad
UVs (0/90/180/270) keep the YUV sampling correct when the
coded frame needs rotation for display.
- FfmpegVideoRenderer swaps the output buffer's width/height for
90/270 streams before super.renderOutputBuffer notifies size,
matching MediaCodecVideoRenderer's post-rotation reporting.
Decoder fallback:
- Renderers.kt selects FfmpegVideoRenderer first when
libffmpegJNI.so is loaded; falls through to the platform path
for formats FFmpeg doesn't handle or ABIs without the .so.
- MediaCodec selector deprioritises every HiSilicon decoder
(OMX.hisi.* and c2.hisi.*) so the platform path picks
c2.android.avc.decoder ahead of the C2 Hisi variant when FFmpeg
isn't available. Required because the C2 Hisi failure is
post-init, which Media3's setEnableDecoderFallback(true) can't
intercept.
Compositor:
- VideoCompositor.setInputSurfaceSize lets the renderer resize the
codec-input SurfaceTexture before eglCreateWindowSurface so the
EGL surface inherits matching buffer dimensions on creation
(MediaCodec sizes natively; EGL doesn't).
- VideoPlayerInstance wires Renderers.build with a sizer callback
that calls into compositor.setInputSurfaceSize from the FFmpeg
renderer thread.
Adds docs/architecture.md with the layered video pipeline diagram,
file map, renderer-selection rationale, build flow, and LGPL
boundary notes.
127 lines
4.2 KiB
Groovy
127 lines
4.2 KiB
Groovy
group 'io.swipelab.ux'
|
|
version '1.0-SNAPSHOT'
|
|
|
|
buildscript {
|
|
ext.kotlin_version = '1.9.22'
|
|
repositories {
|
|
google()
|
|
mavenCentral()
|
|
}
|
|
|
|
dependencies {
|
|
classpath 'com.android.tools.build:gradle:8.1.0'
|
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
}
|
|
}
|
|
|
|
rootProject.allprojects {
|
|
repositories {
|
|
google()
|
|
mavenCentral()
|
|
}
|
|
}
|
|
|
|
apply plugin: 'com.android.library'
|
|
apply plugin: 'kotlin-android'
|
|
|
|
android {
|
|
namespace 'io.swipelab.ux'
|
|
compileSdk 34
|
|
|
|
defaultConfig {
|
|
minSdk 21
|
|
externalNativeBuild {
|
|
cmake {
|
|
cppFlags ""
|
|
}
|
|
}
|
|
}
|
|
|
|
externalNativeBuild {
|
|
cmake {
|
|
path "src/main/jni/CMakeLists.txt"
|
|
}
|
|
}
|
|
|
|
compileOptions {
|
|
sourceCompatibility JavaVersion.VERSION_1_8
|
|
targetCompatibility JavaVersion.VERSION_1_8
|
|
}
|
|
|
|
kotlinOptions {
|
|
jvmTarget = '1.8'
|
|
}
|
|
|
|
sourceSets {
|
|
main {
|
|
// libffmpegJNI.so is built by the buildFfmpegJni task into
|
|
// build/jniLibs/<abi>/ on first build (and any time the
|
|
// vendored ffmpeg_jni.cc / CMakeLists.txt change). Adding
|
|
// the directory here lets AGP package the .so into the
|
|
// AAR without committing native binaries to the repo.
|
|
jniLibs.srcDirs += "$buildDir/jniLibs"
|
|
}
|
|
}
|
|
}
|
|
|
|
// FFmpeg video decoder build — runs as part of the normal Android
|
|
// build. On first build for a given checkout it clones Media3 + FFmpeg
|
|
// into build/ffmpeg-work/ and produces libffmpegJNI.so per ABI (~30 min
|
|
// for the FFmpeg static-lib step the first time, fast after). Gradle
|
|
// UP-TO-DATE checking skips the task whenever the vendored JNI source
|
|
// + CMakeLists are unchanged. See android/ffmpeg/README.md.
|
|
def ffmpegSrcDir = file("$projectDir/ffmpeg")
|
|
def ffmpegWorkDir = file("$buildDir/ffmpeg-work")
|
|
def ffmpegOutDir = file("$buildDir/jniLibs")
|
|
def ndkCmakeBin = "${android.sdkDirectory}/cmake/3.22.1/bin"
|
|
def supportedAbis = ['armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64']
|
|
|
|
task buildFfmpegJni(type: Exec) {
|
|
group = 'build'
|
|
description = 'Clones Media3 + FFmpeg if needed, builds libffmpegJNI.so per Android ABI'
|
|
|
|
inputs.file "$ffmpegSrcDir/ffmpeg_jni.cc"
|
|
inputs.file "$ffmpegSrcDir/CMakeLists.txt"
|
|
inputs.file "$ffmpegSrcDir/build_ffmpeg.sh"
|
|
supportedAbis.each { abi ->
|
|
outputs.file "$ffmpegOutDir/$abi/libffmpegJNI.so"
|
|
}
|
|
|
|
workingDir ffmpegSrcDir
|
|
commandLine 'bash', "$ffmpegSrcDir/build_ffmpeg.sh"
|
|
environment 'JNI_SRC', ffmpegSrcDir.absolutePath
|
|
environment 'NDK_PATH', android.ndkDirectory.absolutePath
|
|
environment 'CMAKE_PATH', ndkCmakeBin
|
|
environment 'OUTPUT_DIR', ffmpegOutDir.absolutePath
|
|
environment 'WORK_DIR', ffmpegWorkDir.absolutePath
|
|
}
|
|
|
|
afterEvaluate {
|
|
preBuild.dependsOn buildFfmpegJni
|
|
}
|
|
|
|
dependencies {
|
|
// CameraX for scanner preview + frame analysis + ux.camera.
|
|
// 1.4.0+ ships 16-KB-aligned `libimage_processing_util_jni.so`
|
|
// (Android 15 requirement); 1.3.x failed the elf-alignment check.
|
|
def cameraxVersion = '1.4.2'
|
|
implementation "androidx.camera:camera-core:$cameraxVersion"
|
|
implementation "androidx.camera:camera-camera2:$cameraxVersion"
|
|
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
|
|
implementation "androidx.camera:camera-view:$cameraxVersion"
|
|
implementation "androidx.camera:camera-video:$cameraxVersion"
|
|
// ProcessLifecycleOwner so CameraInstance can release the camera
|
|
// when the host app backgrounds (and re-acquire on foreground).
|
|
// camera-lifecycle pulls in lifecycle-common but not the process
|
|
// observer; this adds it.
|
|
implementation 'androidx.lifecycle:lifecycle-process:2.7.0'
|
|
// Pure-Kotlin/Java QR decoder. ~470 KB jar, no Play Services dep.
|
|
implementation 'com.google.zxing:core:3.5.3'
|
|
// Media3 for ux.video_player. Same version line video_player_android
|
|
// 2.9.5 pulls in (1.9.x) so the spike fork can coexist during the
|
|
// Phase 2/3 migration without dragging in two ExoPlayer copies.
|
|
def media3Version = '1.9.2'
|
|
implementation "androidx.media3:media3-exoplayer:$media3Version"
|
|
implementation "androidx.media3:media3-common:$media3Version"
|
|
}
|