Files
sm-emby-share/plugins/media3_engine/README.md
T

163 lines
6.9 KiB
Markdown
Raw Normal View History

2026-07-14 11:11:36 +08:00
# 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-opcue 由 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` ++ generationnative
回调先比对此值,过期事件丢弃。这保证了 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 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`, `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 视频走 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。