155 lines
6.1 KiB
Kotlin
155 lines
6.1 KiB
Kotlin
|
|
// 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 行格式:`<sha256> <filename>`,可有 # 开头注释 / 空行
|
|||
|
|
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: '<sha256> $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)
|
|||
|
|
}
|
|||
|
|
}
|