omni_player 0.1.15 copy "omni_player: ^0.1.15" to clipboard
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.52.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/

2
likes
135
points
458
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter媒体播放器插件,在Android/iOS上支持视频和音频播放和后台播放。支持MKV、MP4、HLS等.

Homepage

License

MIT (license)

Dependencies

crypto, flutter, path_provider, plugin_platform_interface

More

Packages that depend on omni_player

Packages that implement omni_player