// Media3 Engine plugin — Android-only ExoPlayer + FFmpeg audio decoder bridge. // // 依赖: // - Maven Central: media3-exoplayer / media3-datasource / media3-decoder 1.10.1 // - 本地 AAR : media3-decoder-ffmpeg-1.10.1-arm64.aar // 由 fileTree 从主 app 的 android/app/libs/ 加载。AAR 由 // scripts/build_media3_ffmpeg.sh 自编译产出,CHECKSUMS.txt // 记录 SHA-256。 // // AAR 缺失时 preBuild 任务会 fail-fast 抛出带恢复指引的错误(详见 README)。 import java.security.MessageDigest plugins { id("com.android.library") id("kotlin-android") } group = "com.smplayer.media3_engine" version = "0.0.1" android { namespace = "com.smplayer.media3_engine" compileSdk = 36 defaultConfig { minSdk = 24 ndk { // 与主 app 保持一致:仅 arm64-v8a。其它 ABI 在 FFmpeg AAR 中也不存在, // 即便 ABI filter 漏了也只是无声的浪费(不会 crash)。 abiFilters += listOf("arm64-v8a") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } buildFeatures { buildConfig = false } } repositories { google() mavenCentral() } // 自编译 FFmpeg audio decoder extension(arm64-v8a only,LGPL 配置)。AAR 由 // scripts/build_media3_ffmpeg.sh 产出,放在 android/app/libs/。 // 这里只声明 compileOnly:AGP 8 起禁止 library module 直接 implementation 本地 // .aar("Direct local .aar file dependencies are not supported when building an // AAR"),实际把 AAR 打入最终 APK 的责任落到主 app/build.gradle.kts 的 // `implementation(fileTree("libs"))`。本 plugin 编译期只需要 FfmpegAudioRenderer // 等符号的可见性。 val media3Version = "1.10.1" val ffmpegAarDir = file("../../../android/app/libs") val ffmpegAarName = "media3-decoder-ffmpeg-$media3Version-arm64.aar" dependencies { implementation("androidx.media3:media3-exoplayer:$media3Version") implementation("androidx.media3:media3-datasource:$media3Version") implementation("androidx.media3:media3-decoder:$media3Version") implementation("androidx.media3:media3-ui:$media3Version") compileOnly(fileTree(mapOf("dir" to ffmpegAarDir, "include" to listOf(ffmpegAarName)))) } // AAR 缺失 / 篡改 fail-fast:在 preBuild 之前校验文件存在性 + SHA-256 与 // `android/app/libs/CHECKSUMS.txt` 一致。CI/Clean checkout 友好。 val verifyMedia3FfmpegAar by tasks.registering { group = "verification" description = "Verify media3 FFmpeg decoder AAR exists and matches CHECKSUMS.txt." val checksumsFile = ffmpegAarDir.resolve("CHECKSUMS.txt") val aarFile = ffmpegAarDir.resolve(ffmpegAarName) inputs.file(aarFile).withPropertyName("aar").withPathSensitivity(org.gradle.api.tasks.PathSensitivity.NONE) inputs.file(checksumsFile).withPropertyName("checksums").withPathSensitivity(org.gradle.api.tasks.PathSensitivity.NONE) doLast { if (!aarFile.isFile) { throw GradleException( """ |Media3 FFmpeg decoder AAR is missing: | expected: ${aarFile.absolutePath} | |This AAR is self-built (LGPL config, arm64-v8a, |codecs: dca/ac3/eac3/truehd/opus/vorbis/flac). |To produce it, run from the repo root: | | scripts/build_media3_ffmpeg.sh | |Prerequisites: NDK r26b, JDK 17, ~30-60min wall-clock. See |plugins/media3_engine/README.md for full instructions and Docker recipe. | |If you only build the desktop (Windows/macOS) target, this Android |module is not on the dependency path and you can ignore this error. """.trimMargin() ) } if (!checksumsFile.isFile) { throw GradleException( "Media3 AAR checksum manifest is missing: ${checksumsFile.absolutePath}\n" + "Restore CHECKSUMS.txt from git or rerun scripts/build_media3_ffmpeg.sh." ) } // CHECKSUMS.txt 行格式:` `,可有 # 开头注释 / 空行 val expectedSha = checksumsFile.readLines() .map { it.trim() } .firstOrNull { it.isNotEmpty() && !it.startsWith("#") && it.endsWith(ffmpegAarName) } ?.substringBefore(' ') ?.trim() ?.lowercase() if (expectedSha.isNullOrBlank() || !expectedSha.matches(Regex("^[0-9a-f]{64}$"))) { throw GradleException( "Media3 AAR checksum entry missing or malformed for $ffmpegAarName in " + "${checksumsFile.absolutePath}. Expected a line: ' $ffmpegAarName'." ) } val md = MessageDigest.getInstance("SHA-256") aarFile.inputStream().use { input -> val buffer = ByteArray(8192) while (true) { val n = input.read(buffer) if (n <= 0) break md.update(buffer, 0, n) } } val actualSha = md.digest().joinToString("") { "%02x".format(it) } if (actualSha != expectedSha) { throw GradleException( """ |Media3 FFmpeg decoder AAR checksum mismatch — refusing to build. | file : ${aarFile.absolutePath} | expected : $expectedSha (from CHECKSUMS.txt) | actual : $actualSha | |The AAR has been replaced/corrupted since CHECKSUMS.txt was written. |If you intentionally rebuilt it, scripts/build_media3_ffmpeg.sh will |refresh CHECKSUMS.txt automatically; otherwise restore the file from git. """.trimMargin() ) } logger.lifecycle("[verifyMedia3FfmpegAar] OK — sha256 $actualSha") } } afterEvaluate { tasks.named("preBuild").configure { dependsOn(verifyMedia3FfmpegAar) } }