omni_player 1.0.0 copy "omni_player: ^1.0.0" to clipboard
omni_player: ^1.0.0 copied to clipboard

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

OmniPlayer #

OmniPlayer 是一个面向 Flutter 的跨平台媒体播放器插件,基于 VLC 引擎封装 iOS 与 Android 播放能力,支持视频、音频、后台播放、锁屏/通知栏控制、精确 seek、倍速、循环播放与可选磁盘缓存。

特性 #

  • 跨平台播放:支持 iOS 与 Android,底层分别使用 MobileVLCKit / libVLC。
  • 音视频一体:支持视频、纯音频、后台音频、锁屏/通知栏媒体控制。
  • 广泛格式:支持 MP4、MOV、MKV、MP3、AAC、FLAC、WAV、HLS、DASH、RTSP 等 VLC 可播放格式。
  • 高频交互优化:插件内部处理 seek、暂停、恢复播放、状态乱序和进度回退问题。
  • 播放控制完整:支持播放、暂停、停止、seek、音量、倍速、循环播放、进度回调频率控制。
  • 可选磁盘缓存:普通 HTTP/HTTPS 点播资源可后台缓存,再次播放直接命中本地文件。

平台实现 #

平台 播放引擎 视频渲染 后台/控制
iOS MobileVLCKit VLC drawable + UiKitView MPRemoteCommandCenter
Android libVLC Flutter Texture Foreground Service + MediaSessionCompat

当前仅支持 iOS 与 Android,不支持 macOS、Windows、Web。

安装 #

flutter pub add omni_player

或手动添加到 pubspec.yaml

dependencies:
  omni_player: ^0.1.23

平台配置 #

iOS #

ios/Runner/Info.plist 按需添加:

<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
</array>

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>
  • UIBackgroundModes 用于后台音频播放。
  • NSAppTransportSecurity 仅在需要播放 HTTP 明文资源时添加。
  • 真机运行需要有效的开发者证书和 Provisioning Profile。

Android #

android/app/src/main/AndroidManifest.xml 确保有网络权限:

<uses-permission android:name="android.permission.INTERNET" />

插件会自动注册前台播放服务与媒体控制相关组件。

快速开始 #

初始化 #

import 'package:omni_player/omni_player.dart';

final player = OmniPlayer.instance;

Future<void> initPlayer() async {
  await player.initialize(
    cacheConfig: const CacheConfig(
      enabled: true,
      maxSize: 500 * 1024 * 1024,
    ),
  );
}

播放媒体 #

await player.open(
  const MediaItem(
    url: 'https://example.com/video.mp4',
    title: '示例视频',
    artist: 'OmniPlayer',
    coverUrl: 'https://example.com/cover.jpg',
    isVideo: true,
  ),
);

显示视频 #

AspectRatio(
  aspectRatio: player.videoSize?.aspectRatio ?? 16 / 9,
  child: VideoWidget(
    player: player,
    fit: BoxFit.contain,
  ),
)

释放资源 #

@override
void dispose() {
  player.dispose();
  super.dispose();
}

OmniPlayer.instance 是全局单例。调用 dispose() 后如需再次使用,需要重新获取 OmniPlayer.instance 并调用 initialize()

完整示例 #

import 'dart:async';
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;
  final _subs = <StreamSubscription>[];

  PlayerState _state = PlayerState.idle;
  Duration _position = Duration.zero;
  Duration _duration = Duration.zero;
  double _buffered = 0.0;

  @override
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    await _player.initialize(
      cacheConfig: const CacheConfig(enabled: true),
    );

    _subs.addAll([
      _player.stateStream.listen((value) => setState(() => _state = value)),
      _player.positionStream.listen((value) => setState(() => _position = value)),
      _player.durationStream.listen((value) => setState(() => _duration = value)),
      _player.bufferedStream.listen((value) => setState(() => _buffered = value)),
    ]);

    await _player.open(
      const MediaItem(
        url: 'https://example.com/video.mp4',
        title: '示例视频',
        isVideo: true,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final durationMs = _duration.inMilliseconds;
    final progress = durationMs > 0
        ? (_position.inMilliseconds / durationMs).clamp(0.0, 1.0)
        : 0.0;

    return Scaffold(
      backgroundColor: Colors.black,
      body: Column(
        children: [
          AspectRatio(
            aspectRatio: _player.videoSize?.aspectRatio ?? 16 / 9,
            child: VideoWidget(player: _player),
          ),
          Slider(
            value: progress,
            secondaryTrackValue: _buffered.clamp(0.0, 1.0),
            onChanged: durationMs <= 0
                ? null
                : (value) => _player.seek(
                      Duration(milliseconds: (value * durationMs).round()),
                    ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              IconButton(
                color: Colors.white,
                icon: const Icon(Icons.replay_10),
                onPressed: () => _player.seek(_position - const Duration(seconds: 10)),
              ),
              IconButton(
                color: Colors.white,
                iconSize: 42,
                icon: Icon(_state == PlayerState.playing ? Icons.pause : Icons.play_arrow),
                onPressed: _state == PlayerState.playing ? _player.pause : _player.play,
              ),
              IconButton(
                color: Colors.white,
                icon: const Icon(Icons.forward_10),
                onPressed: () => _player.seek(_position + const Duration(seconds: 10)),
              ),
            ],
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    for (final sub in _subs) {
      sub.cancel();
    }
    _player.dispose();
    super.dispose();
  }
}

常用场景 #

播放音频并响应锁屏/通知栏上下首 #

final player = OmniPlayer.instance;

await player.initialize();

player.previousTrackStream.listen((_) => playPrevious());
player.nextTrackStream.listen((_) => playNext());

await player.open(
  const MediaItem(
    url: 'https://example.com/audio.mp3',
    title: '示例音频',
    artist: 'Artist',
    album: 'Album',
    coverUrl: 'https://example.com/cover.jpg',
    isVideo: false,
  ),
);

自定义 HTTP 请求头 #

await player.open(
  const MediaItem(
    url: 'https://private.example.com/video.m3u8',
    title: '鉴权视频',
    isVideo: true,
    headers: {
      'Authorization': 'Bearer token',
      'Referer': 'https://example.com',
    },
  ),
);

循环播放与倍速 #

await player.setLooping(true);
await player.setSpeed(1.5);
await player.open(
  const MediaItem(
    url: 'https://example.com/lesson.mp4',
    title: '课程视频',
    isVideo: true,
  ),
);

调整进度回调频率 #

await player.setPositionUpdateInterval(const Duration(milliseconds: 100));

普通播放建议保持默认 500ms;歌词同步、精细进度条等场景可降低间隔。

缓存管理 #

await player.initialize(
  cacheConfig: const CacheConfig(
    enabled: true,
    maxSize: 300 * 1024 * 1024,
  ),
);

final size = await player.getCacheSizeString();
final cached = await player.cacheManager?.isCached('https://example.com/video.mp4') ?? false;

await player.clearCacheItem('https://example.com/video.mp4');
await player.clearCache();

API 参考 #

OmniPlayer #

生命周期

方法 说明
initialize({CacheConfig? cacheConfig}) 初始化播放器并订阅原生事件;使用前必须调用。
dispose() 释放原生资源、事件流与缓存管理器。

播放控制

方法 说明
open(MediaItem item, {bool autoPlay = true}) 打开媒体;默认自动播放。
play() 播放或恢复播放。
pause() 暂停播放。
stop() 停止播放并清除当前媒体。
seek(Duration position) 跳转到指定时间点。
setVolume(double volume) 设置音量,范围 0.0 ~ 1.0
setSpeed(double speed) 设置倍速,例如 0.751.01.52.0
setLooping(bool looping) 设置是否循环播放。
setPositionUpdateInterval(Duration interval) 设置进度回调频率,默认 500ms。

同步属性

属性 类型 说明
state PlayerState 当前播放状态。
position Duration 当前播放进度。
duration Duration 媒体总时长。
buffered double 缓冲进度,范围 0.0 ~ 1.0
looping bool 当前是否循环播放。
textureId int? Android Texture ID;iOS 通常为 null
videoSize VideoSize? 视频尺寸。
error String? 最近一次错误信息。
cacheManager CacheManager? 缓存管理器;未启用缓存时为 null

事件流

Stream 类型 说明
stateStream Stream<PlayerState> 播放状态变化。
positionStream Stream<Duration> 播放进度变化。
durationStream Stream<Duration> 媒体总时长变化。
bufferedStream Stream<double> 缓冲进度变化。
textureIdStream Stream<int?> Android Texture ID 变化。
videoSizeStream Stream<VideoSize> 视频尺寸变化。
errorStream Stream<String> 播放错误。
previousTrackStream Stream<void> 通知栏/锁屏/耳机线控上一首事件。
nextTrackStream Stream<void> 通知栏/锁屏/耳机线控下一首事件。

MediaItem #

字段 类型 说明
url String 媒体地址,支持 HTTP/HTTPS、本地文件、HLS、DASH、RTSP 等 VLC 支持的地址。
title String 媒体标题,用于通知栏/锁屏显示。
artist String? 艺术家。
album String? 专辑。
coverUrl String? 封面图地址。
isVideo bool 是否为视频;默认 false
headers Map<String, String>? 自定义 HTTP 请求头。

PlayerState #

状态 说明
idle 空闲,未加载媒体。
loading 加载或缓冲中。
playing 播放中。
paused 已暂停。
stopped 已停止。
completed 播放完成。
error 播放错误。

CacheConfig #

字段 类型 默认值 说明
enabled bool true 是否启用缓存。
maxSize int 500 * 1024 * 1024 最大缓存空间,单位字节。
customDirectory String? null 自定义缓存目录;为空时使用系统缓存目录下的 omni_player_cache

VideoWidget #

VideoWidget({
  required OmniPlayer player,
  BoxFit fit = BoxFit.contain,
  Color backgroundColor = Colors.black,
})
  • iOS 使用 UiKitView 承载 VLC drawable。
  • Android 使用 Flutter Texture 显示 VLC 输出。
  • fit 仅影响 Android Texture 的缩放方式;iOS 由原生 drawable 承载。
  • backgroundColor 默认为黑色。

缓存策略 #

  • 仅普通 HTTP/HTTPS 点播资源参与缓存。
  • HLS(.m3u8)、DASH(.mpd)、RTSP、RTMP 等流媒体协议会自动跳过缓存。
  • 首次播放仍走远端,同时后台下载;下载完成后再次播放才会命中本地缓存。
  • 缓存存放在系统 Cache 目录,系统存储不足时可能被 OS 自动清理。
  • dispose() 会刷写缓存元数据;一般无需手动处理。

使用建议 #

  • initialize() 完成后再调用 open()
  • 同一时间只播放一个媒体;再次 open() 会停止当前媒体。
  • 视频场景必须设置 MediaItem.isVideo: true 并在界面中放置 VideoWidget
  • 纯音频请设置 isVideo: false,可减少纹理、PlatformView 和 GPU 资源占用。
  • 播放列表切换建议监听 completednextTrackStream 后由业务层调用 open()
  • seek() 是精确跳转,关键帧间隔较大的视频可能需要更长时间完成解码。

常见问题 #

iOS 视频黑屏 #

确认 MediaItem.isVideotrue,并且页面中使用了 VideoWidget。iOS 视频由 VLC drawable 直接渲染,普通 Flutter 容器无法显示画面。

Android 视频黑屏 #

确认已经调用 initialize()open(),并且 MediaItem.isVideotrue。Android 通过 Texture 渲染,VideoWidget 会自动监听 textureIdStream

播放状态一直是 loading #

网络资源首次缓冲可能较慢。本地文件一直 loading 时,请检查文件路径是否存在、URL 是否正确、格式是否被 VLC 支持。

上一首/下一首没有自动切换 #

previousTrackStreamnextTrackStream 只发出事件,不会自动切换媒体。业务层需要在监听器中自行调用 player.open()

缓存命中但没有走本地文件 #

确认初始化时传入了 CacheConfig(enabled: true)。首次播放只会后台下载,缓存完成后下次播放才会命中。

缓冲进度看起来等于播放进度 #

本地文件、小文件或网络较快时,缓冲可能很快到 100%。部分流媒体协议无法精确提供完整缓冲窗口,插件会尽量估算。

iOS 无法截图视频画面 #

iOS 使用 VLC drawable PlatformView 渲染,GPU 内容无法被 Flutter RenderRepaintBoundary 捕获,这是平台限制。

License #

详见 LICENSE

2
likes
0
points
96
downloads

Publisher

unverified uploader

Weekly Downloads

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

Homepage

License

unknown (license)

Dependencies

crypto, flutter, path_provider, plugin_platform_interface

More

Packages that depend on omni_player

Packages that implement omni_player