# 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 @ /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::` / `auto` / `no`) | – | | `setSubtitleTrack` | `playerId, id` (`text::` / `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 非必需) ### 步骤 ```sh # 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 验证加载 ``` ### 重新编译 / 升级版本 ```sh # 完全清空工作区重新拉取(覆盖 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) ```sh 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 | ## 平台守卫四层 1. **枚举层**:`PlayerEngineOverride.media3` / `PlayerEngineKind.media3` 在所有平台编译通过(inert) 2. **路由层**:`resolvePlayerEngineKind` 在 desktop 把 media3 降级为 mpv 3. **构造层**:`Media3PlayerEngine` 构造函数 `assert(Platform.isAndroid)`,否则抛 `UnsupportedError` 4. **UI 层**:`PlayerEngineRow.visibleOptions` 过滤 + `coerceValue` 持久化值降级 任一层失守,其它层仍能阻止 native crash。