Files
2026-07-14 11:11:36 +08:00
..
2026-07-14 11:11:36 +08:00
2026-07-14 11:11:36 +08:00
2026-07-14 11:11:36 +08:00
2026-07-14 11:11:36 +08:00

media3_engine

smplayer 项目本地 Flutter plugin。仅 Android,包名 com.smplayer.media3_engine 不发布到 pub.dev。Dart 侧通过约定好的 MethodChannel / EventChannel 与 native 通讯。

引擎实现位于主项目:

  • Dartlib/core/infra/player_engine/media3_player_engine.dart
  • Kotlinplugins/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_READYPlaybackException 后 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-opcue 由 Dart 渲染)
getProperty playerId, name stringffmpegAvailable / 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 ++ generationnative 回调先比对此值,过期事件丢弃。这保证了 cancel / reopen 时不会有迟到的 STATE_READY 污染新 session 的状态。

FFmpeg 软解 codec 列表

scripts/build_media3_ffmpeg.shENABLED_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 hostApple Silicon 建议通过 Docker linux-x86_64 容器编译以规避 Media3 README 未文档化的 darwin-arm64 行为)
  • Android NDK r27d (LTS):从 https://developer.android.com/ndk/downloads 安装;Media3 1.10.1 README 实测版本是 r26br27 ABI 兼容、社区已验证可编
  • perl, make, yasm, git, python3, shasummacOS 自带;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
音量 boost100 < volume ≤ 200 ExoPlayer volume 上限为 1.0;本插件把 100..200 段静默截断到 1.0。Dart 端 state.volume 保留原值(UI 一致),但物理音量不会更大
HDR / DV 路由层显式排除(resolvePlayerEngineKind 在 DV/ProRes 下回退 fvp);media3 视频走 MediaCodecDV 元数据未被特殊处理
iOS / 桌面 plugin 仅声明 Android 平台,构造层 Platform.isAndroid 守卫;UI 层桌面隐藏 media3 选项
PiP / MediaSession / Cast 不实现(范围外);播放在前台 Activity 内完成
DRM / Widevine 不实现(范围外)
FFmpeg AAR 体积 约 12-15 MB / arm64-v8arelease APK 增量约 ≤ 30 MB

平台守卫四层

  1. 枚举层PlayerEngineOverride.media3 / PlayerEngineKind.media3 在所有平台编译通过(inert
  2. 路由层resolvePlayerEngineKind 在 desktop 把 media3 降级为 mpv
  3. 构造层Media3PlayerEngine 构造函数 assert(Platform.isAndroid),否则抛 UnsupportedError
  4. UI 层PlayerEngineRow.visibleOptions 过滤 + coerceValue 持久化值降级

任一层失守,其它层仍能阻止 native crash。