omni_player 0.1.15
omni_player: ^0.1.15 copied to clipboard
Flutter媒体播放器插件,在Android/iOS上支持视频和音频播放和后台播放。支持MKV、MP4、HLS等.
OmniPlayer 播放器插件说明文档 #
概述 #
OmniPlayer 是一个跨平台 Flutter 媒体播放器插件,支持 iOS 和 Android。
| 平台 | 底层引擎 | 视频渲染 |
|---|---|---|
| iOS | MobileVLCKit | Metal PlatformView(无像素拷贝) |
| Android | ExoPlayer (Media3) | Flutter Texture API |
仅支持 iOS 和 Android,不支持 macOS / Windows。
支持格式:MP4、MKV、MOV、AVI、MP3、AAC、FLAC、OGG、HLS(.m3u8)、DASH、RTSP 等 VLC/ExoPlayer 支持的所有格式。
安装 #
在 pubspec.yaml 中引入插件:
dependencies:
omni_player:
path: ../omni_player # 或 pub.dev 地址
iOS 配置 #
ios/Runner/Info.plist 添加:
<!-- 后台音频播放 -->
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<!-- 允许 HTTP 明文请求(按需添加) -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Android 配置 #
android/app/src/main/AndroidManifest.xml 确保有以下权限(Service 声明由插件自动注册):
<uses-permission android:name="android.permission.INTERNET" />
快速开始 #
1. 初始化 #
import 'package:omni_player/omni_player.dart';
final player = OmniPlayer.instance;
@override
void initState() {
super.initState();
_initPlayer();
}
Future<void> _initPlayer() async {
await player.initialize(
// 可选:启用磁盘缓存(点播场景推荐开启)
cacheConfig: const CacheConfig(
enabled: true,
maxSize: 500 * 1024 * 1024, // 500 MB
),
);
}
2. 播放媒体 #
await player.open(
MediaItem(
url: 'https://example.com/video.mp4',
title: '我的视频',
artist: '作者',
album: '专辑',
coverUrl: 'https://example.com/cover.jpg',
isVideo: true,
),
);
3. 显示视频画面 #
VideoWidget(
player: player,
fit: BoxFit.contain,
backgroundColor: Colors.black,
)
4. 释放资源 #
@override
void dispose() {
player.dispose();
super.dispose();
}
API 文档 #
OmniPlayer(单例) #
OmniPlayer.instance // 获取全局单例
初始化 / 释放
| 方法 | 说明 |
|---|---|
initialize({CacheConfig? cacheConfig}) |
启动播放器,订阅事件流(必须在使用前调用);传入 cacheConfig 可启用磁盘缓存 |
dispose() |
释放所有原生资源,关闭事件流,刷写未落盘的缓存元数据 |
播放控制
| 方法 | 说明 |
|---|---|
open(MediaItem, {autoPlay}) |
打开媒体,autoPlay 默认 true |
play() |
播放 |
pause() |
暂停 |
stop() |
停止并清除媒体 |
seek(Duration) |
跳转到指定位置 |
setVolume(double) |
设置音量,范围 0.0 ~ 1.0 |
setSpeed(double) |
设置倍速,例如 1.5、2.0 |
setLooping(bool) |
是否循环播放 |
setPositionUpdateInterval(Duration) |
设置进度回调频率,默认 500ms |
缓存操作
| 方法 | 说明 |
|---|---|
getCacheSize() |
返回当前缓存占用字节数 Future<int>,未启用时返回 0 |
getCacheSizeString() |
返回格式化字符串 Future<String>,如 "123.4 MB",未启用时返回 "0 B" |
clearCache() |
清空所有缓存文件 |
clearCacheItem(String url) |
清除指定 URL 的缓存 |
cacheManager |
底层 CacheManager?,可访问更多进阶配置 |
状态属性(同步读取当前值)
| 属性 | 类型 | 说明 |
|---|---|---|
state |
PlayerState |
当前播放状态 |
position |
Duration |
当前播放位置 |
duration |
Duration |
媒体总时长 |
buffered |
double |
缓冲进度 0.0 ~ 1.0 |
textureId |
int? |
Android Texture ID(iOS 为 null) |
videoSize |
VideoSize? |
视频分辨率 |
error |
String? |
最近一次错误信息 |
事件流(Stream)
| 流 | 类型 | 说明 |
|---|---|---|
stateStream |
Stream<PlayerState> |
播放状态变化 |
positionStream |
Stream<Duration> |
播放进度变化 |
durationStream |
Stream<Duration> |
时长更新(首次解析后触发) |
bufferedStream |
Stream<double> |
缓冲进度变化 |
textureIdStream |
Stream<int?> |
Texture ID 变化(仅 Android) |
videoSizeStream |
Stream<VideoSize> |
视频分辨率变化 |
errorStream |
Stream<String> |
播放错误信息 |
previousTrackStream |
Stream<void> |
用户点击通知栏/锁屏「上一首」 |
nextTrackStream |
Stream<void> |
用户点击通知栏/锁屏「下一首」 |
MediaItem #
MediaItem({
required String url, // 媒体地址(HTTP/HTTPS/本地路径)
required String title, // 标题(显示在通知栏/锁屏)
String? artist, // 艺术家
String? album, // 专辑
String? coverUrl, // 封面图 URL(显示在通知栏/锁屏)
bool isVideo = false, // true = 视频;false = 纯音频
Map<String, String>? headers, // 自定义 HTTP 请求头
})
PlayerState 枚举 #
enum PlayerState {
idle, // 空闲,未加载任何媒体
loading, // 加载/缓冲中
playing, // 播放中
paused, // 已暂停
stopped, // 已停止(调用 stop() 后)
completed, // 播放完成(非循环模式到达末尾)
error, // 发生错误
}
CacheConfig #
CacheConfig({
bool enabled = true, // 是否启用缓存
int maxSize = 500 * 1024 * 1024, // 最大磁盘空间(字节),默认 500 MB,超出时 LRU 自动淘汰
String? customDirectory, // 自定义缓存目录,null = 系统 Cache 目录/omni_player_cache
})
缓存策略:仅对普通 HTTP/HTTPS 点播 URL 生效,HLS(
.m3u8)、DASH(.mpd)、RTSP、RTMP 等流媒体协议自动跳过缓存。
VideoWidget #
VideoWidget({
required OmniPlayer player,
BoxFit fit = BoxFit.contain, // 视频填充模式
Color backgroundColor = Colors.black, // 背景色
})
iOS 使用 Metal PlatformView 渲染,不支持 Flutter 截图(GPU 内容无法被
renderRepaintBoundary捕获)。
完整示例 #
示例一:播放视频 #
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:omni_player/omni_player.dart';
class VideoPlayerPage extends StatefulWidget {
const VideoPlayerPage({super.key});
@override
State<VideoPlayerPage> createState() => _VideoPlayerPageState();
}
class _VideoPlayerPageState extends State<VideoPlayerPage> {
final _player = OmniPlayer.instance;
PlayerState _state = PlayerState.idle;
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
double _buffered = 0.0;
final List<StreamSubscription> _subs = [];
@override
void initState() {
super.initState();
_initPlayer();
}
Future<void> _initPlayer() async {
await _player.initialize(
cacheConfig: const CacheConfig(
enabled: true,
maxSize: 500 * 1024 * 1024,
),
);
_subs.addAll([
_player.stateStream.listen((s) => setState(() => _state = s)),
_player.positionStream.listen((p) => setState(() => _position = p)),
_player.durationStream.listen((d) => setState(() => _duration = d)),
_player.bufferedStream.listen((b) => setState(() => _buffered = b)),
// 监听锁屏/通知栏上下集按钮
_player.previousTrackStream.listen((_) => _playPrevious()),
_player.nextTrackStream.listen((_) => _playNext()),
]);
await _player.open(
const MediaItem(
url: 'https://example.com/video.mp4',
title: '示例视频',
artist: '演示',
coverUrl: 'https://example.com/cover.jpg',
isVideo: true,
),
);
}
void _playPrevious() { /* 切换到上一个媒体 */ }
void _playNext() { /* 切换到下一个媒体 */ }
String _fmt(Duration d) {
final m = d.inMinutes.remainder(60).toString().padLeft(2, '0');
final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
return '${d.inHours > 0 ? '${d.inHours}:' : ''}$m:$s';
}
@override
Widget build(BuildContext context) {
// iOS 不发 textureId,只要 isVideo=true 就直接显示 VideoWidget
final bool isIOS = defaultTargetPlatform == TargetPlatform.iOS;
final bool hasVideo = isIOS || _player.textureId != null;
return Scaffold(
backgroundColor: Colors.black,
body: Column(
children: [
// 视频画面
AspectRatio(
aspectRatio: _player.videoSize?.aspectRatio ?? 16 / 9,
child: hasVideo
? VideoWidget(player: _player)
: const Center(child: CircularProgressIndicator()),
),
// 进度条(secondaryTrackValue 显示缓冲进度)
Slider(
value: _duration.inMilliseconds > 0
? _position.inMilliseconds / _duration.inMilliseconds
: 0.0,
secondaryTrackValue: _buffered,
onChanged: (v) => _player.seek(
Duration(milliseconds: (v * _duration.inMilliseconds).toInt()),
),
),
// 时间显示
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_fmt(_position), style: const TextStyle(color: Colors.white)),
Text(_fmt(_duration), style: const TextStyle(color: Colors.white)),
],
),
),
// 控制按钮
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.replay_10, color: Colors.white),
onPressed: () => _player.seek(_position - const Duration(seconds: 10)),
),
IconButton(
icon: Icon(
_state == PlayerState.playing ? Icons.pause : Icons.play_arrow,
color: Colors.white, size: 40,
),
onPressed: _state == PlayerState.playing ? _player.pause : _player.play,
),
IconButton(
icon: const Icon(Icons.forward_10, color: Colors.white),
onPressed: () => _player.seek(_position + const Duration(seconds: 10)),
),
],
),
],
),
);
}
@override
void dispose() {
for (final sub in _subs) sub.cancel();
_player.dispose();
super.dispose();
}
}
示例二:播放音频(后台 + 锁屏控制) #
Future<void> playAudio() async {
final player = OmniPlayer.instance;
await player.initialize();
// 监听上下首(锁屏耳机线控 / 通知栏按钮)
player.previousTrackStream.listen((_) => playPrev());
player.nextTrackStream.listen((_) => playNext());
await player.open(
const MediaItem(
url: 'https://example.com/music.mp3',
title: '轻音乐',
artist: 'Artist Name',
album: 'Album Name',
coverUrl: 'https://example.com/cover.jpg',
isVideo: false, // 纯音频,不创建视频渲染资源
),
);
}
示例三:自定义 HTTP 请求头(带鉴权的私有流) #
await player.open(
MediaItem(
url: 'https://private-cdn.example.com/stream.m3u8',
title: '私有直播',
isVideo: true,
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Referer': 'https://example.com',
},
),
);
示例四:进度回调频率控制 #
// 歌词同步等高精度场景,设置 100ms
await player.setPositionUpdateInterval(const Duration(milliseconds: 100));
// 普通场景节省性能,设置 1000ms
await player.setPositionUpdateInterval(const Duration(seconds: 1));
示例五:循环播放 + 倍速 #
await player.setLooping(true);
await player.setSpeed(1.5);
await player.open(
const MediaItem(
url: 'https://example.com/lesson.mp4',
title: '教学视频',
isVideo: true,
),
);
示例六:播放列表 #
class PlaylistPage extends StatefulWidget {
const PlaylistPage({super.key});
@override
State<PlaylistPage> createState() => _PlaylistPageState();
}
class _PlaylistPageState extends State<PlaylistPage> {
final _player = OmniPlayer.instance;
final _playlist = const [
MediaItem(url: 'https://example.com/ep1.mp4', title: '第 1 集', isVideo: true),
MediaItem(url: 'https://example.com/ep2.mp4', title: '第 2 集', isVideo: true),
MediaItem(url: 'https://example.com/ep3.mp4', title: '第 3 集', isVideo: true),
];
int _currentIndex = 0;
final List<StreamSubscription> _subs = [];
@override
void initState() {
super.initState();
_initPlayer();
}
Future<void> _initPlayer() async {
await _player.initialize(
cacheConfig: const CacheConfig(enabled: true),
);
_subs.addAll([
// 播放完成后自动播放下一集
_player.stateStream.listen((s) {
if (s == PlayerState.completed) _playNext();
}),
// 通知栏/锁屏上下集按钮
_player.previousTrackStream.listen((_) => _playPrev()),
_player.nextTrackStream.listen((_) => _playNext()),
]);
_playAt(0);
}
Future<void> _playAt(int index) async {
setState(() => _currentIndex = index);
await _player.open(_playlist[index]);
}
void _playPrev() {
if (_currentIndex > 0) _playAt(_currentIndex - 1);
}
void _playNext() {
if (_currentIndex < _playlist.length - 1) _playAt(_currentIndex + 1);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: VideoWidget(player: _player),
),
// 播放列表
Expanded(
child: ListView.builder(
itemCount: _playlist.length,
itemBuilder: (_, i) => ListTile(
title: Text(_playlist[i].title),
selected: i == _currentIndex,
onTap: () => _playAt(i),
),
),
),
],
),
);
}
@override
void dispose() {
for (final sub in _subs) sub.cancel();
_player.dispose();
super.dispose();
}
}
示例七:缓存管理 #
// 启用缓存(初始化时配置一次即可)
await player.initialize(
cacheConfig: const CacheConfig(
enabled: true,
maxSize: 300 * 1024 * 1024, // 300 MB
),
);
// ── 查看缓存占用 ──────────────────────────────
final sizeStr = await player.getCacheSizeString();
print('当前缓存:$sizeStr'); // 例如 "87.3 MB"
// ── 显示在设置页 UI ───────────────────────────
Text('缓存占用:$sizeStr')
// ── 清空所有缓存 ──────────────────────────────
await player.clearCache();
// ── 清除单个视频缓存 ──────────────────────────
await player.clearCacheItem('https://example.com/video.mp4');
// ── 动态调整最大空间 ──────────────────────────
await player.cacheManager?.setMaxSize(200 * 1024 * 1024); // 改为 200 MB
注意事项 #
iOS #
- 真机运行需要有效的开发者证书和 Provisioning Profile
- 视频使用 Metal PlatformView 渲染,不支持截图(
renderRepaintBoundary无法捕获 GPU 内容) - 后台音频需在
Info.plist配置UIBackgroundModes: [audio] - 锁屏控制(上下首 / 播放暂停 / 进度拖拽)通过
MPRemoteCommandCenter实现,无需额外配置
Android #
- 通知栏媒体控制(⏮ 播放/暂停 ⏭)由 Media3
MediaSessionService自动管理 - 点击通知栏上下集按钮会触发
previousTrackStream/nextTrackStream,不会自动切换曲目,需业务层在监听器里调用player.open()实现 - 封面图通过 Glide 异步加载,加载完成后自动刷新通知栏(仅 HTTPS URL 有效)
- Android 12+ 需要应用处于前台或拥有
FOREGROUND_SERVICE权限才能持续显示通知
通用 #
OmniPlayer.instance是全局单例,dispose()后若需再次使用,需重新调用initialize()VideoWidget必须与对应的OmniPlayer实例搭配使用isVideo: false时不创建视频纹理 / PlatformView,节省内存和 GPU 资源- 同时只能播放一个媒体;调用
open()会自动停止当前媒体 seek()使用精确模式,跳转到指定时间点而非最近关键帧;对于关键帧间隔大的视频,精确 seek 需要从上一关键帧解码到目标帧,耗时略长属正常现象
缓存 #
- 缓存仅对 HTTP/HTTPS 点播 URL 生效;HLS(
.m3u8)、DASH(.mpd)、RTSP、RTMP 自动跳过 - 首次播放仍走远端网络,后台下载完成后再次播放才会命中缓存
- 缓存文件存放在系统 Cache 目录,系统存储不足时 可能被 OS 自动清理,属正常行为
- 调用
clearCache()或clearCacheItem()后,下次播放该资源将重新下载并缓存 dispose()时会自动将未落盘的元数据刷写到磁盘,无需手动调用
常见问题 #
Q:iOS 视频黑屏?
A:确认 isVideo: true 且界面中使用了 VideoWidget。iOS 必须用 VideoWidget(PlatformView)渲染,普通 Container 无效。
Q:Android 通知栏没有封面?
A:确认 MediaItem.coverUrl 是可公开访问的 HTTPS 图片 URL,不支持本地路径。
Q:播放状态一直是 loading? A:网络流媒体首次缓冲耗时较长属正常。本地文件若一直 loading,请检查文件路径和格式是否受支持。
Q:上下首按钮点了没反应?
A:previousTrackStream / nextTrackStream 只负责将事件通知到 Flutter 层,具体的切换逻辑需要业务代码自行实现(在监听器中调用 player.open())。
Q:iOS 缓冲进度和播放进度一样? A:小文件在快速网络下会瞬间下载完毕,缓冲进度会直接跳到 100%,这是正常行为。
Q:后台音频暂停了?
A:iOS 需要在 Info.plist 添加 UIBackgroundModes: [audio]。Android 需确保 PlayerService 处于前台(插件已自动处理)。
Q:缓存命中了但视频没有播放?
A:确认 initialize() 传入了 CacheConfig(enabled: true),未传则缓存不会启用。Android 端首次冷启动需等待 Service 绑定完成(initialize() 返回后才可调用 open(),已在插件内部保证)。
Q:第二次播放仍然走网络,缓存没有生效? A:首次播放会在后台静默下载,下载完成前关闭 App 会导致缓存未写入。再次打开 App 播放一遍后即可正常缓存。
Q:如何知道某个视频是否已缓存?
A:使用底层 API 检测:await player.cacheManager?.isCached(url) ?? false。
Q:缓存文件存在哪里?
A:默认存放在系统应用缓存目录的 omni_player_cache 子目录下。Android 路径约为 /data/data/{包名}/cache/omni_player_cache/,iOS 约为 Library/Caches/omni_player_cache/。