Files
sm-emby-share/plugins/media3_engine/README.md
T
2026-07-14 11:11:36 +08:00

163 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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。