# omni_player
**Repository Path**: sirli369/omni_player
## Basic Information
- **Project Name**: omni_player
- **Description**: OmniPlayer 是一个跨平台 Flutter 媒体播放器插件,支持 iOS 和 Android。
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-03-19
- **Last Updated**: 2026-03-30
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 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` 中引入插件:
```yaml
dependencies:
omni_player:
path: ../omni_player # 或 pub.dev 地址
```
### iOS 配置
`ios/Runner/Info.plist` 添加:
```xml
UIBackgroundModes
audio
NSAppTransportSecurity
NSAllowsArbitraryLoads
```
### Android 配置
`android/app/src/main/AndroidManifest.xml` 确保有以下权限(Service 声明由插件自动注册):
```xml
```
---
## 快速开始
### 1. 初始化
```dart
import 'package:omni_player/omni_player.dart';
final player = OmniPlayer.instance;
@override
void initState() {
super.initState();
_initPlayer();
}
Future _initPlayer() async {
await player.initialize(
// 可选:启用磁盘缓存(点播场景推荐开启)
cacheConfig: const CacheConfig(
enabled: true,
maxSize: 500 * 1024 * 1024, // 500 MB
),
);
}
```
### 2. 播放媒体
```dart
await player.open(
MediaItem(
url: 'https://example.com/video.mp4',
title: '我的视频',
artist: '作者',
album: '专辑',
coverUrl: 'https://example.com/cover.jpg',
isVideo: true,
),
);
```
### 3. 显示视频画面
```dart
VideoWidget(
player: player,
fit: BoxFit.contain,
backgroundColor: Colors.black,
)
```
### 4. 释放资源
```dart
@override
void dispose() {
player.dispose();
super.dispose();
}
```
---
## API 文档
### OmniPlayer(单例)
```dart
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`,未启用时返回 `0` |
| `getCacheSizeString()` | 返回格式化字符串 `Future`,如 `"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` | 播放状态变化 |
| `positionStream` | `Stream` | 播放进度变化 |
| `durationStream` | `Stream` | 时长更新(首次解析后触发) |
| `bufferedStream` | `Stream` | 缓冲进度变化 |
| `textureIdStream` | `Stream` | Texture ID 变化(仅 Android) |
| `videoSizeStream` | `Stream` | 视频分辨率变化 |
| `errorStream` | `Stream` | 播放错误信息 |
| `previousTrackStream` | `Stream` | 用户点击通知栏/锁屏「上一首」 |
| `nextTrackStream` | `Stream` | 用户点击通知栏/锁屏「下一首」 |
---
### MediaItem
```dart
MediaItem({
required String url, // 媒体地址(HTTP/HTTPS/本地路径)
required String title, // 标题(显示在通知栏/锁屏)
String? artist, // 艺术家
String? album, // 专辑
String? coverUrl, // 封面图 URL(显示在通知栏/锁屏)
bool isVideo = false, // true = 视频;false = 纯音频
Map? headers, // 自定义 HTTP 请求头
})
```
---
### PlayerState 枚举
```dart
enum PlayerState {
idle, // 空闲,未加载任何媒体
loading, // 加载/缓冲中
playing, // 播放中
paused, // 已暂停
stopped, // 已停止(调用 stop() 后)
completed, // 播放完成(非循环模式到达末尾)
error, // 发生错误
}
```
---
### CacheConfig
```dart
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
```dart
VideoWidget({
required OmniPlayer player,
BoxFit fit = BoxFit.contain, // 视频填充模式
Color backgroundColor = Colors.black, // 背景色
})
```
> iOS 使用 Metal PlatformView 渲染,不支持 Flutter 截图(GPU 内容无法被 `renderRepaintBoundary` 捕获)。
---
## 完整示例
### 示例一:播放视频
```dart
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 createState() => _VideoPlayerPageState();
}
class _VideoPlayerPageState extends State {
final _player = OmniPlayer.instance;
PlayerState _state = PlayerState.idle;
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
double _buffered = 0.0;
final List _subs = [];
@override
void initState() {
super.initState();
_initPlayer();
}
Future _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();
}
}
```
---
### 示例二:播放音频(后台 + 锁屏控制)
```dart
Future 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 请求头(带鉴权的私有流)
```dart
await player.open(
MediaItem(
url: 'https://private-cdn.example.com/stream.m3u8',
title: '私有直播',
isVideo: true,
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Referer': 'https://example.com',
},
),
);
```
---
### 示例四:进度回调频率控制
```dart
// 歌词同步等高精度场景,设置 100ms
await player.setPositionUpdateInterval(const Duration(milliseconds: 100));
// 普通场景节省性能,设置 1000ms
await player.setPositionUpdateInterval(const Duration(seconds: 1));
```
---
### 示例五:循环播放 + 倍速
```dart
await player.setLooping(true);
await player.setSpeed(1.5);
await player.open(
const MediaItem(
url: 'https://example.com/lesson.mp4',
title: '教学视频',
isVideo: true,
),
);
```
---
### 示例六:播放列表
```dart
class PlaylistPage extends StatefulWidget {
const PlaylistPage({super.key});
@override
State createState() => _PlaylistPageState();
}
class _PlaylistPageState extends State {
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 _subs = [];
@override
void initState() {
super.initState();
_initPlayer();
}
Future _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 _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();
}
}
```
---
### 示例七:缓存管理
```dart
// 启用缓存(初始化时配置一次即可)
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/`。