media3_engine
smplayer 项目本地 Flutter plugin。仅 Android,包名 com.smplayer.media3_engine,
不发布到 pub.dev。Dart 侧通过约定好的 MethodChannel / EventChannel 与 native 通讯。
引擎实现位于主项目:
- Dart:
lib/core/infra/player_engine/media3_player_engine.dart - Kotlin:
plugins/media3_engine/android/src/main/kotlin/com/smplayer/media3_engine/
版本锁定
| 组件 | 版本 |
|---|---|
| androidx.media | 1.10.1 |
| FFmpeg | release/6.0 |
| Android NDK | r27d LTS (27.3.13750724) |
Android minSdk |
24 |
Android compileSdk |
36 |
| AGP / Kotlin / JDK | 8.11.1 / 2.2.20 / 17 |
| ABI | arm64-v8a only(包体最小化) |
| License | LGPL(不开 --enable-gpl / --enable-nonfree) |
升 Media3 / FFmpeg 版本前,先核对 androidx/media @ <tag>/libraries/decoder_ffmpeg/README.md
推荐的 NDK 与 FFmpeg branch,再调整 scripts/build_media3_ffmpeg.sh 默认值。
通道协议
MethodChannel smplayer/media3_engine
| 方法 | 参数 | 返回 |
|---|---|---|
create |
– | {playerId:int, textureId:int, ffmpegAvailable:bool, ffmpegVersion:string} |
open |
playerId, url, headers, startMs, play, generation |
首次 STATE_READY 或 PlaybackException 后 complete |
play / pause |
playerId |
– |
stop |
playerId, generation |
– |
seek |
playerId, positionMs |
– |
setRate |
playerId, rate (float) |
– |
setVolume |
playerId, volume (0..200) |
– |
setAudioTrack |
playerId, id (audio:<g>:<t> / auto / no) |
– |
setSubtitleTrack |
playerId, id (text:<g>:<t> / auto / no) |
– |
setSubtitleStyle |
playerId, fontSize, bgOpacity, position |
– (首版 no-op,cue 由 Dart 渲染) |
getProperty |
playerId, name |
string(ffmpegAvailable / ffmpegVersion / audioRenderer / playbackState) |
dispose |
playerId |
– |
EventChannel smplayer/media3_engine/events
所有事件载荷形如 {playerId, generation, type, ...},Dart 侧按 playerId 过滤、
按 generation 丢弃过期事件。
type |
payload |
|---|---|
position |
{positionMs, bufferMs} |
duration |
{durationMs} |
playing |
{playing} |
completed |
{} |
videoSize |
{width, height} |
tracks |
{audio:[{id,index,title,language}], subtitle:[…]} |
subtitle |
{lines:[string]} |
volume |
{volume}(保留,目前 native 不主动 emit) |
error |
{code, message} |
Generation token 语义:Dart 端每次 open / stop / dispose ++ generation,native
回调先比对此值,过期事件丢弃。这保证了 cancel / reopen 时不会有迟到的 STATE_READY
污染新 session 的状态。
FFmpeg 软解 codec 列表
scripts/build_media3_ffmpeg.sh 的 ENABLED_DECODERS 默认:
| 解码器名(FFmpeg) | 实际格式 |
|---|---|
dca |
DTS / DTS-HD / DTS-HD MA |
ac3 |
AC3 |
eac3 |
E-AC3 (Dolby Digital Plus) |
truehd |
Dolby TrueHD(含 Atmos 元数据,但 Atmos 渲染依赖宿主 audio sink) |
opus |
Opus |
vorbis |
Vorbis |
flac |
FLAC |
视频解码不在范围——交给平台 MediaCodec 硬解。容器解析与音频帧 parser 由 ExoPlayer
自身处理,FFmpeg 扩展只补 decoder(无需 --enable-parser / --enable-demuxer)。
编译 AAR
前置硬约束:
android/app/libs/media3-decoder-ffmpeg-1.10.1-arm64.aar是 Android 构建的硬依赖。仓库默认不提交该 AAR 二进制;缺文件时 plugin 的preBuild任务verifyMedia3FfmpegAar会 fail-fast 抛错并给出本节的恢复 指令。CI / Clean checkout 都必须先编出 AAR 才能flutter build apk。
前置
- macOS / Linux host(Apple Silicon 建议通过 Docker
linux-x86_64容器编译以规避 Media3 README 未文档化的 darwin-arm64 行为) - Android NDK r27d (LTS):从 https://developer.android.com/ndk/downloads 安装;Media3 1.10.1 README 实测版本是 r26b,r27 ABI 兼容、社区已验证可编
perl,make,yasm,git,python3,shasum(macOS 自带;Linux 需手装 yasm)- JDK 17 + Android SDK(命令行工具足够,Android Studio 非必需)
步骤
# 1. 设置 NDK 路径(或用 --ndk-path 参数)
export ANDROID_NDK_HOME=/path/to/android-ndk-r27d
# 2. 跑脚本(首次约 30-60 分钟,取决于 host CPU / IO)
./scripts/build_media3_ffmpeg.sh
# 3. 产物
# android/app/libs/media3-decoder-ffmpeg-1.10.1-arm64.aar
# android/app/libs/CHECKSUMS.txt 自动追加 / 替换该 AAR 的 SHA-256
#
# 4. 核对 SHA-256 → git add 上述两个文件 → flutter build apk --debug 验证加载
重新编译 / 升级版本
# 完全清空工作区重新拉取(覆盖 androidx/media + FFmpeg 缓存)
./scripts/build_media3_ffmpeg.sh --clean
# 升 Media3 tag
./scripts/build_media3_ffmpeg.sh --clean --media3-tag 1.11.0
# 升 FFmpeg branch
./scripts/build_media3_ffmpeg.sh --clean --ffmpeg-branch release/6.1
脚本会在 libraries/decoder_ffmpeg/src/main/jni/build_ffmpeg.sh 上幂等地打一次
sed patch,把上游默认的 4 ABI 限制到 arm64-v8a only。--clean 会清空 patch 与缓存。
Docker 复现编译(推荐 Apple Silicon)
docker run --rm -it \
-v "$(pwd)":/work \
-e ANDROID_NDK_HOME=/opt/android-ndk-r27d \
-w /work \
cimg/android:2024.05.1-ndk \
scripts/build_media3_ffmpeg.sh
镜像版本以 r27d NDK 实际可用性为准;上面只是一个起点示意。
已知限制
| 限制 | 说明 / 解决路径 |
|---|---|
| 位图字幕(PGS / VobSub) | onCues 检出后 emit 空文本 + error: 'subtitle_bitmap_unsupported';建议改外挂 SRT/ASS 或切回 fvp |
| 音量 boost(100 < volume ≤ 200) | ExoPlayer volume 上限为 1.0;本插件把 100..200 段静默截断到 1.0。Dart 端 state.volume 保留原值(UI 一致),但物理音量不会更大 |
| HDR / DV | 路由层显式排除(resolvePlayerEngineKind 在 DV/ProRes 下回退 fvp);media3 视频走 MediaCodec,DV 元数据未被特殊处理 |
| iOS / 桌面 | plugin 仅声明 Android 平台,构造层 Platform.isAndroid 守卫;UI 层桌面隐藏 media3 选项 |
| PiP / MediaSession / Cast | 不实现(范围外);播放在前台 Activity 内完成 |
| DRM / Widevine | 不实现(范围外) |
| FFmpeg AAR 体积 | 约 12-15 MB / arm64-v8a;release APK 增量约 ≤ 30 MB |
平台守卫四层
- 枚举层:
PlayerEngineOverride.media3/PlayerEngineKind.media3在所有平台编译通过(inert) - 路由层:
resolvePlayerEngineKind在 desktop 把 media3 降级为 mpv - 构造层:
Media3PlayerEngine构造函数assert(Platform.isAndroid),否则抛UnsupportedError - UI 层:
PlayerEngineRow.visibleOptions过滤 +coerceValue持久化值降级
任一层失守,其它层仍能阻止 native crash。