FFmpeg software H.264 decoder: opt-in via pubspec flag
- Gate buildFfmpegJni + jniLibs packaging on `ux: enable_ffmpeg` in the consuming app's pubspec (default off) -- no LGPL / H.264-patent exposure unless explicitly enabled - appInfoBuilder generates kUxEnableFfmpeg from the same flag so apps register the FFmpeg LGPL notice eagerly, pubspec-only (no dart-define) - Add registerFfmpegLicense() + bundled LGPL-2.1 text asset - FFmpeg compliance docs (LICENSES-3RDPARTY.md, android/ffmpeg/README.md) - Network video streaming: XVideoPlayerController.network Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,9 +24,59 @@ rootProject.allprojects {
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
// FFmpeg software H.264 decoder — OPT-IN, disabled by default.
|
||||
//
|
||||
// The decoder (a fallback for devices whose hardware decoder fails) carries
|
||||
// LGPL + H.264 patent obligations, so it is only bundled when the consuming
|
||||
// app asks for it, via a flag in its pubspec.yaml:
|
||||
//
|
||||
// ux:
|
||||
// enable_ffmpeg: true
|
||||
//
|
||||
// (or -Pux.enable_ffmpeg=true on the Gradle command line as a CI override).
|
||||
// When off: buildFfmpegJni is skipped, no libffmpegJNI.so is packaged,
|
||||
// FfmpegLibrary.isAvailable() returns false, the renderer is never added, and
|
||||
// playback uses the platform MediaCodec decoders only. See android/ffmpeg/README.md.
|
||||
//
|
||||
// This pubspec flag is the only switch: ux's build_runner builder also reads
|
||||
// `ux: enable_ffmpeg` and generates a `kUxEnableFfmpeg` constant the app uses
|
||||
// to register the LGPL license notice — no --dart-define needed.
|
||||
def ffmpegEnabled = {
|
||||
// CI / command-line override takes precedence over the pubspec flag.
|
||||
def prop = project.findProperty('ux.enable_ffmpeg')
|
||||
if (prop != null) return prop.toString().trim().equalsIgnoreCase('true')
|
||||
|
||||
// rootProject is the consuming app's android/ project; its pubspec.yaml
|
||||
// sits one directory up. Match a top-level `ux:` block (not the indented
|
||||
// `ux:` dependency entry) and its nested `enable_ffmpeg:` value.
|
||||
def pubspec = new File(rootProject.projectDir.parentFile, 'pubspec.yaml')
|
||||
if (!pubspec.exists()) return false
|
||||
boolean inUxBlock = false
|
||||
boolean enabled = false
|
||||
pubspec.eachLine { raw ->
|
||||
def hash = raw.indexOf('#')
|
||||
def line = hash >= 0 ? raw.substring(0, hash) : raw
|
||||
if (line.trim().isEmpty()) return
|
||||
if (!Character.isWhitespace(line.charAt(0))) {
|
||||
// A new top-level key: we're in the config block only if it's `ux:`.
|
||||
inUxBlock = line.trim().startsWith('ux:')
|
||||
} else if (inUxBlock) {
|
||||
def t = line.trim()
|
||||
if (t.startsWith('enable_ffmpeg:')) {
|
||||
enabled = t.substring('enable_ffmpeg:'.length()).trim().equalsIgnoreCase('true')
|
||||
}
|
||||
}
|
||||
}
|
||||
return enabled
|
||||
}()
|
||||
|
||||
android {
|
||||
namespace 'io.swipelab.ux'
|
||||
compileSdk 34
|
||||
// media3 1.9.2 requires compileSdk >= 35; match the app/Flutter default (36).
|
||||
compileSdk 36
|
||||
// Match Flutter's pinned NDK (flutter_tools gradle_utils.dart) and the
|
||||
// app; AGP 8.1's default NDK (25.x) isn't installed on dev machines.
|
||||
ndkVersion '28.2.13676358'
|
||||
|
||||
defaultConfig {
|
||||
minSdk 21
|
||||
@@ -55,21 +105,23 @@ android {
|
||||
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
|
||||
// build/jniLibs/<abi>/. Only reference it when FFmpeg is enabled,
|
||||
// so a stale .so from a previous enabled build is not packaged
|
||||
// into a now-disabled build. Lets AGP package the .so into the
|
||||
// AAR without committing native binaries to the repo.
|
||||
jniLibs.srcDirs += "$buildDir/jniLibs"
|
||||
if (ffmpegEnabled) {
|
||||
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.
|
||||
// FFmpeg video decoder build (opt-in — see the `ffmpegEnabled` flag near the
|
||||
// top of this file and android/ffmpeg/README.md). When enabled, the first
|
||||
// build for a checkout 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 it when the
|
||||
// vendored JNI source + CMakeLists are unchanged.
|
||||
def ffmpegSrcDir = file("$projectDir/ffmpeg")
|
||||
def ffmpegWorkDir = file("$buildDir/ffmpeg-work")
|
||||
def ffmpegOutDir = file("$buildDir/jniLibs")
|
||||
@@ -97,7 +149,13 @@ task buildFfmpegJni(type: Exec) {
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
preBuild.dependsOn buildFfmpegJni
|
||||
if (ffmpegEnabled) {
|
||||
preBuild.dependsOn buildFfmpegJni
|
||||
logger.lifecycle("ux: FFmpeg software H.264 decoder ENABLED (ux: enable_ffmpeg: true).")
|
||||
} else {
|
||||
logger.lifecycle("ux: FFmpeg software H.264 decoder disabled (default). " +
|
||||
"Add 'ux:\\n enable_ffmpeg: true' to the app pubspec.yaml to bundle it.")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -7,13 +7,34 @@ slotted ahead of `MediaCodecVideoRenderer` so iOS H.264 streams with
|
||||
deep DPB (`has_b_frames > 8`) and full-range YUV play on devices where
|
||||
the platform decoder fails (notably Huawei Mate 20 on EMUI 11).
|
||||
|
||||
## Opt-in
|
||||
|
||||
FFmpeg is **disabled by default** (it carries LGPL + H.264 patent
|
||||
obligations). A consuming app enables it with a flag in its `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
ux:
|
||||
enable_ffmpeg: true
|
||||
```
|
||||
|
||||
(or `-Pux.enable_ffmpeg=true` on the Gradle command line as a CI override).
|
||||
When disabled, no `libffmpegJNI.so` is built or packaged, `FfmpegLibrary
|
||||
.isAvailable()` returns false, the renderer is never added, and playback
|
||||
uses the platform MediaCodec decoders only.
|
||||
|
||||
The pubspec flag is the **only** switch. The in-app LGPL license notice
|
||||
follows automatically: ux's build_runner builder ([lib/builder.dart](../../lib/builder.dart))
|
||||
reads `ux: enable_ffmpeg` from the app's pubspec and generates
|
||||
`kUxEnableFfmpeg` into the app's `app_info.g.dart`; the app gates
|
||||
`registerFfmpegLicense()` on that constant at startup. No `--dart-define`, no
|
||||
duplication — and the notice shows without needing to play a video.
|
||||
|
||||
## How it builds
|
||||
|
||||
The native library is produced by the `:ux:buildFfmpegJni` Gradle task,
|
||||
wired as a dependency of `preBuild`. On any consumer build
|
||||
(`flutter build apk`, `./gradlew assembleRelease`, IDE sync) the task
|
||||
runs automatically; Gradle's UP-TO-DATE checking skips it when nothing
|
||||
relevant changed.
|
||||
When enabled, the native library is produced by the `:ux:buildFfmpegJni`
|
||||
Gradle task, wired as a dependency of `preBuild`. The task runs on a
|
||||
consumer build (`flutter build apk`, `./gradlew assembleRelease`, IDE
|
||||
sync); Gradle's UP-TO-DATE checking skips it when nothing relevant changed.
|
||||
|
||||
What the task does:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user