omni_player 2.1.0
omni_player: ^2.1.0 copied to clipboard
Flutter媒体播放器插件,在Android/iOS上支持视频和音频播放和后台播放。支持MKV、MP4、HLS等.
OmniPlayer #
Flutter 跨平台音视频播放器插件,基于 media_kit(libmpv / FFmpeg)封装,专为 iOS 与 Android 生产环境设计。
核心能力:边下边播(本地代理流式缓存)、后台长时间稳定播放、锁屏 / 通知栏控制、断线自动重连、播放列表预缓存。
特性 #
播放能力 #
- 广泛格式:MP4、MKV、AVI、MOV、FLV、WebM、MP3、AAC、FLAC、OGG、WAV 及 FFmpeg 可解码的任何格式
- 流媒体协议:HLS(.m3u8)、DASH(.mpd)、RTSP、RTMP
- 精确 Seek:libmpv 高精度帧级跳转,任意倍速下延迟 < 200 ms
- 倍速播放:0.25x ~ 4.0x,倍速正确同步到锁屏进度条
- 视频截图:调用
screenshot-raw获取当前帧,支持 JPEG / PNG 编码输出
边下边播(Streaming Cache Proxy) #
- 首次播放通过本地 HTTP 代理(
127.0.0.1)桥接远端资源 - 代理同步下载文件到磁盘、同步服务给 libmpv,实现真正的边下边播
- MP4 moov atom 在末尾(非 faststart)时自动透传 Range 请求,不影响主下载
- 下载完成后原子重命名写入缓存,下次播放直接读本地
file://,完全无网络请求
后台稳定播放 #
- iOS:所有播放请求发往
127.0.0.1(loopback),iOS 永不回收本地连接,彻底规避ENOTCONN / ffurl_read错误 - Android:前台服务(Foreground Service)持续持有 CPU 唤醒锁,回前台时自动修复黑屏问题(EGL context 重建)
- 网络错误时指数退避自动重连(1 → 2 → 4 → 8 → 16 秒,最多 5 次),重连后从断点续播
系统集成 #
- iOS:
AVAudioSession(.playback类别)+MPRemoteCommandCenter+MPNowPlayingInfoCenter - Android:
MediaSessionCompat+MediaStyle通知栏,支持播放 / 暂停 / 上一首 / 下一首 / 进度拖动 - 锁屏大图、标题、艺术家、进度、倍速均正确同步
缓存管理 #
- LRU 淘汰策略,总大小超过上限时自动删除最久未访问的条目
- 防抖元数据写入(500 ms debounce),避免频繁 I/O
- 支持自定义缓存目录、动态调整容量上限、单条 / 全量清理
API 设计 #
- 与 media_kit
player.state.*/player.stream.*命名完全一致,零改动迁移 - 单例
OmniPlayer.instance,全局唯一,自动管理生命周期
平台支持 #
| 平台 | 播放引擎 | 视频渲染 | 后台 / 控制 |
|---|---|---|---|
| iOS ≥ 12.0 | libmpv (FFmpeg) | Metal Texture | AVAudioSession + MPRemoteCommandCenter |
| Android API ≥ 21 | libmpv (FFmpeg) | OpenGL ES Texture | Foreground Service + MediaSessionCompat |
仅支持 iOS 与 Android,不支持 macOS、Windows、Web。
安装 #
dependencies:
omni_player: ^2.0.0
# 消费端 App 必须引入媒体解码库(包含 FFmpeg)
media_kit_libs_video: ^1.0.4
flutter pub get
平台配置 #
iOS — Info.plist #
<!-- 后台音频播放(必须) -->
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<!-- 播放 HTTP 明文资源时添加(可选) -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Android — AndroidManifest.xml #
插件已在自身 manifest 内声明前台服务与 MediaSession,宿主 App 只需保留网络权限:
<uses-permission android:name="android.permission.INTERNET" />
快速开始 #
1. 全局初始化 #
在 main() 中调用一次,早于任何播放操作:
import 'package:media_kit/media_kit.dart';
import 'package:omni_player/omni_player.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized(); // media_kit 要求
runApp(const MyApp());
}
2. 初始化播放器 #
在页面 initState 中调用,传入缓存配置以启用边下边播:
final player = OmniPlayer.instance;
await player.initialize(
cacheConfig: const CacheConfig(
enabled: true,
maxSize: 500 * 1024 * 1024, // 500 MB
),
);
3. 打开媒体 #
await player.open(
const MediaItem(
url: 'https://example.com/video.mp4',
title: '示例视频',
artist: '作者',
coverUrl: 'https://example.com/cover.jpg',
isVideo: true,
headers: {
'Authorization': 'Bearer token', // 可选的 HTTP 请求头
},
),
autoPlay: true,
);
缓存行为:
- 已缓存 → 直接读本地
file://,无网络请求 - 未缓存 → 通过本地代理边下边播,下载完成后自动写入缓存
skipCache: true→ 强制走网络,忽略缓存
4. 视频渲染 #
使用内置 VideoWidget,自带加载指示器:
VideoWidget(
player: OmniPlayer.instance,
fit: BoxFit.contain, // 填充模式
backgroundColor: Colors.black,
)
或使用 media_kit 原生 Video Widget(需要 player.videoController):
import 'package:media_kit_video/media_kit_video.dart';
Video(controller: player.videoController!)
5. 播放控制 #
await player.play();
await player.pause();
await player.stop();
await player.seek(const Duration(seconds: 30));
await player.setVolume(80.0); // 0.0 ~ 100.0
await player.setRate(1.5); // 倍速,同 media_kit setRate()
await player.setSpeed(1.5); // setRate() 的别名
await player.setLooping(true);
截取当前视频帧(media_kit 1.2.6+ 内置,委托 libmpv screenshot-raw):
final bytes = await player.captureVideoFrame(); // JPEG(默认)
// or
final pngBytes = await player.captureVideoFrame(format: 'image/png');
// or 获取 BGRA 裸像素数据
final rawPixels = await player.captureVideoFrame(format: null);
if (bytes != null) {
await File('frame.jpg').writeAsBytes(bytes);
}
6. 状态读取 #
快照方式(与 media_kit player.state.* 一致):
final player = OmniPlayer.instance;
player.state.playing // bool
player.state.buffering // bool
player.state.completed // bool
player.state.position // Duration
player.state.duration // Duration
player.state.buffer // Duration(已缓冲时长)
player.state.bufferingPercentage // double 0.0~1.0
player.state.volume // double 0~100
player.state.rate // double
player.state.width // int?
player.state.height // int?
player.state.error // String
player.state.status // PlaybackStatus 枚举
流订阅方式(与 media_kit player.stream.* 一致):
player.stream.playing.listen((v) => print('playing: $v'));
player.stream.position.listen((p) => print('pos: $p'));
player.stream.duration.listen((d) => print('dur: $d'));
player.stream.buffering.listen((v) => print('buffering: $v'));
player.stream.bufferingPercentage.listen((p) => print('buf: ${(p*100).toInt()}%'));
player.stream.completed.listen((v) { if (v) playNext(); });
player.stream.error.listen((e) => print('error: $e'));
player.stream.status.listen((s) => print('status: $s'));
// 锁屏 / 通知栏的上一首 / 下一首命令(media_kit 无对应)
player.stream.previousTrack.listen((_) => playPrevious());
player.stream.nextTrack.listen((_) => playNext());
7. 播放列表预缓存 #
在播放当前视频时,提前缓存下一首,实现切换时秒开:
// 开始播放 videos[index] 后立即预缓存 videos[index+1]
player.preCache(videos[index + 1].url);
preCache 通过本地代理在后台下载,不影响当前播放,完成后自动写入 LRU 缓存。
8. 缓存管理 #
final size = await player.getCacheSizeString(); // "128.5 MB"
final bytes = await player.getCacheSize(); // int(字节)
await player.clearCacheItem('https://example.com/video.mp4');
await player.clearCache(); // 清空所有缓存
9. 释放资源 #
// 通常在页面 dispose() 中调用
await player.dispose();
API 参考 #
OmniPlayer #
| 方法 / 属性 | 类型 | 说明 |
|---|---|---|
OmniPlayer.instance |
OmniPlayer |
获取全局单例 |
initialize({CacheConfig?}) |
Future<void> |
初始化播放器、原生服务、缓存代理 |
open(MediaItem, {autoPlay, skipCache, audioOnly}) |
Future<void> |
打开并(可选)自动播放媒体 |
play() |
Future<void> |
播放 |
pause() |
Future<void> |
暂停 |
stop() |
Future<void> |
停止并清除当前媒体 |
seek(Duration) |
Future<void> |
跳转到指定位置 |
setVolume(double) |
Future<void> |
设置音量(0.0 ~ 100.0) |
setRate(double) |
Future<void> |
设置倍速(对应 media_kit setRate()) |
setSpeed(double) |
Future<void> |
setRate() 的别名 |
setLooping(bool) |
Future<void> |
是否循环播放 |
setPositionUpdateInterval(Duration) |
Future<void> |
设置进度推送频率(默认 500ms) |
captureVideoFrame({String? format}) |
Future<Uint8List?> |
截取当前视频帧;format 支持 'image/jpeg'(默认)、'image/png'、null(BGRA 裸数据)。纯音频返回 null |
preCache(String url, {Map<String,String>? headers}) |
void |
后台预缓存指定 URL |
keepAudioSessionAlive() |
void |
手动唤醒 iOS AVAudioSession(一般无需调用) |
state |
OmniPlayerState |
当前状态快照(media_kit 兼容命名) |
stream |
OmniPlayerStream |
事件流集合(media_kit 兼容命名) |
position |
Duration |
当前播放位置 |
duration |
Duration |
媒体总时长 |
buffered |
double |
缓冲进度 0.0 ~ 1.0 |
currentItem |
MediaItem? |
当前媒体 |
videoController |
VideoController? |
media_kit VideoController(供 Video Widget 使用) |
cacheManager |
CacheManager? |
缓存管理器(直接操作缓存时使用) |
getCacheSize() |
Future<int> |
缓存大小(字节) |
getCacheSizeString() |
Future<String> |
缓存大小(可读字符串,如 "128.5 MB") |
clearCache() |
Future<void> |
清空全部缓存 |
clearCacheItem(String url) |
Future<void> |
清除指定 URL 的缓存 |
dispose() |
Future<void> |
释放全部资源 |
MediaItem #
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url |
String |
✅ | 媒体地址(http://、https://、file://) |
title |
String |
✅ | 标题(显示在锁屏 / 通知栏) |
artist |
String? |
艺术家 | |
album |
String? |
专辑 | |
coverUrl |
String? |
封面图 URL(锁屏大图) | |
isVideo |
bool |
是否为视频(默认 false);true 时进后台自动切音频轨 |
|
headers |
Map<String,String>? |
自定义 HTTP 请求头 |
CacheConfig #
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
bool |
true |
是否启用缓存与边下边播代理 |
maxSize |
int |
500 MB |
最大缓存空间(字节),超出时 LRU 淘汰 |
customDirectory |
String? |
null |
自定义缓存目录;null 使用系统应用缓存目录 |
PlaybackStatus 枚举 #
| 值 | 说明 |
|---|---|
idle |
未加载任何媒体 |
loading |
正在加载 / 缓冲 |
playing |
正在播放 |
paused |
已暂停 |
stopped |
已停止 |
completed |
播放完毕 |
error |
发生错误 |
边下边播原理 #
open(url)
├─ 已缓存 → file:// 直接播放(零网络请求)
└─ 未缓存 → http://127.0.0.1:{port}/?u={url}
│
StreamingProxy
┌────────────────────────────────────┐
│ ① 向远端发起单次 HTTP 下载 │
│ ② 同步写入 cacheDir/hash.ext.tmp │ ← 边存
│ ③ 从 tmp 文件服务给 libmpv │ ← 边播
│ ④ moov 乱序请求 → 直接透传远端 │
│ ⑤ 完成后原子重命名 → hash.ext │
│ ⑥ 通知 CacheManager 建立 LRU 索引 │
└────────────────────────────────────┘
下次播放 → 命中缓存 → file:// 直接播放
为什么用 loopback 代理解决 iOS 后台断连:
iOS 在 App 进入后台后会回收非 loopback 的 TCP 连接,libmpv 直连远端时会收到 ENOTCONN(错误码 0xFFFFFC7)导致播放中断。代理运行在 127.0.0.1,iOS 永不回收 loopback 连接;远端的 TCP 由代理管理,即使被系统回收也由代理自行重连,libmpv 完全感知不到网络波动。
注意事项 #
media_kit_libs_video必须由消费端 App 引入,插件本身不内嵌 FFmpeg 库(避免重复打包增大体积)。- 缓存与边下边播仅对
http:///https://点播资源生效;HLS(.m3u8)、DASH(.mpd)、RTSP、RTMP 不缓存(分片流不适合整文件缓存)。 isVideo: true时进后台自动禁用视频轨道(保留音频)、回前台自动恢复;isVideo: false无此切换开销,适用于纯音频场景。setVolume参数范围 0.0 ~ 100.0(与 media_kit 一致),而非 0.0 ~ 1.0。- Android 最低 API 21,iOS 最低 12.0。