163 lines
6.9 KiB
Markdown
163 lines
6.9 KiB
Markdown
|
|
# 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 非必需)
|
|||
|
|
|
|||
|
|
### 步骤
|
|||
|
|
|
|||
|
|
```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。
|