进阶实战 Flutter for OpenHarmony:video_player 第三方库实战 - 专业级视频播放
在移动互联网时代,视频内容已经成为应用中最受欢迎的媒体形式之一。无论是短视频应用、在线教育平台、企业培训系统,还是社交媒体应用,视频播放功能都是不可或缺的核心功能。想象一下这样的场景:用户打开你的在线教育应用,浏览课程列表,点击感兴趣的课程视频。视频开始加载,用户可以看到播放进度条、控制按钮,可以随时暂停、快进、调节播放速度。整个播放过程流畅自然,用户可以专注于学习内容,而不是被糟糕的播放体验所困

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🔍 一、第三方库概述与应用场景
📱 1.1 为什么需要视频播放功能?
在移动互联网时代,视频内容已经成为应用中最受欢迎的媒体形式之一。无论是短视频应用、在线教育平台、企业培训系统,还是社交媒体应用,视频播放功能都是不可或缺的核心功能。
想象一下这样的场景:用户打开你的在线教育应用,浏览课程列表,点击感兴趣的课程视频。视频开始加载,用户可以看到播放进度条、控制按钮,可以随时暂停、快进、调节播放速度。整个播放过程流畅自然,用户可以专注于学习内容,而不是被糟糕的播放体验所困扰。
这就是 video_player 库要解决的问题。它提供了一套完整的视频播放解决方案,让开发者可以轻松实现跨平台的视频播放功能,同时保持良好的用户体验。
📋 1.2 video_player 是什么?
video_player 是 Flutter 官方维护的核心插件,专门用于在应用内播放视频内容。它支持多种视频源(网络视频、本地文件、Asset 资源),提供了丰富的播放控制功能,并且可以与其他 Flutter Widget 无缝集成。
在 OpenHarmony 平台上,video_player 同样提供了完整的支持,让开发者可以无缝地使用这套 API 来实现视频播放功能。
🎯 1.3 核心功能特性
| 功能特性 | 详细说明 | OpenHarmony 支持 |
|---|---|---|
| 网络视频播放 | 支持 HTTP/HTTPS 协议的网络视频 | ✅ 完全支持 |
| 本地文件播放 | 播放设备存储中的视频文件 | ✅ 完全支持 |
| Asset 资源播放 | 播放应用内置的视频资源 | ✅ 完全支持 |
| 播放控制 | 播放、暂停、跳转、速度调节 | ✅ 完全支持 |
| 循环播放 | 设置视频循环播放 | ✅ 完全支持 |
| 状态监听 | 实时监听播放状态和进度 | ✅ 完全支持 |
| 视频信息获取 | 获取时长、宽高比等信息 | ✅ 完全支持 |
💡 1.4 典型应用场景
在实际的应用开发中,video_player 有着广泛的应用场景:
短视频应用:用户可以浏览、播放短视频内容,支持滑动切换、自动播放等功能。
在线教育平台:学生可以观看课程视频,支持进度记录、倍速播放、章节跳转等功能。
企业培训系统:员工可以观看培训视频,支持考核、进度追踪等功能。
社交媒体应用:用户可以分享和观看视频动态,支持点赞、评论、转发等社交功能。
🏗️ 二、系统架构设计
📐 2.1 整体架构
为了构建一个可维护、可扩展的视频播放系统,我们采用分层架构设计:
┌─────────────────────────────────────────────────────────┐
│ UI 层 (展示层) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 播放界面 │ │ 控制面板 │ │ 进度条 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 服务层 (业务逻辑) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ VideoPlayerService │ │
│ │ • 统一的视频播放接口 │ │
│ │ • 播放状态管理 │ │
│ │ • 进度追踪与记录 │ │
│ │ • 错误处理与重试 │ │
│ └─────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 基础设施层 (底层实现) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ video_player 插件 │ │
│ │ • VideoPlayerController - 视频控制器 │ │
│ │ • VideoPlayer - 视频显示组件 │ │
│ │ • VideoPlayerValue - 播放状态值 │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
📊 2.2 数据模型设计
为了更好地管理视频播放的状态和配置,我们设计了一套数据模型:
/// 视频源类型
enum VideoSourceType {
network, // 网络视频
file, // 本地文件
asset, // Asset 资源
}
/// 视频播放状态
enum VideoPlayState {
idle, // 空闲
loading, // 加载中
ready, // 准备就绪
playing, // 播放中
paused, // 已暂停
buffering, // 缓冲中
error, // 错误
completed, // 播放完成
}
/// 视频信息模型
class VideoInfo {
/// 视频标题
final String title;
/// 视频描述
final String? description;
/// 视频源地址
final String source;
/// 视频源类型
final VideoSourceType sourceType;
/// 视频封面
final String? thumbnail;
/// 视频时长
final Duration? duration;
const VideoInfo({
required this.title,
this.description,
required this.source,
required this.sourceType,
this.thumbnail,
this.duration,
});
}
/// 播放进度信息
class PlaybackProgress {
/// 当前播放位置
final Duration position;
/// 视频总时长
final Duration duration;
/// 缓冲进度
final Duration buffered;
/// 播放速度
final double speed;
const PlaybackProgress({
required this.position,
required this.duration,
required this.buffered,
this.speed = 1.0,
});
/// 播放进度百分比 (0.0 - 1.0)
double get progress {
if (duration.inMilliseconds == 0) return 0;
return position.inMilliseconds / duration.inMilliseconds;
}
/// 缓冲进度百分比 (0.0 - 1.0)
double get bufferedProgress {
if (duration.inMilliseconds == 0) return 0;
return buffered.inMilliseconds / duration.inMilliseconds;
}
/// 格式化的当前位置
String get formattedPosition => _formatDuration(position);
/// 格式化的总时长
String get formattedDuration => _formatDuration(duration);
static String _formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
if (hours > 0) {
return '${hours.toString().padLeft(2, '0')}:'
'${minutes.toString().padLeft(2, '0')}:'
'${seconds.toString().padLeft(2, '0')}';
}
return '${minutes.toString().padLeft(2, '0')}:'
'${seconds.toString().padLeft(2, '0')}';
}
}
📦 三、项目配置与依赖安装
📥 3.1 添加依赖
在 Flutter 项目中使用 video_player,需要在 pubspec.yaml 文件中添加依赖。由于我们要支持 OpenHarmony 平台,需要使用适配版本的仓库。
打开项目根目录下的 pubspec.yaml 文件,找到 dependencies 部分,添加以下配置:
dependencies:
flutter:
sdk: flutter
# video_player - 视频播放插件
# 使用 OpenHarmony 适配版本
video_player:
git:
url: "https://atomgit.com/openharmony-tpc/flutter_packages.git"
path: "packages/video_player/video_player"
配置说明:
git方式引用:因为 OpenHarmony 适配版本需要从指定的 Git 仓库获取url:指向开源鸿蒙 TPC 维护的 flutter_packages 仓库path:指定仓库中 video_player 包的具体路径- 本项目基于
video_player@2.7.1开发,适配 Flutter 3.27.5-ohos-1.0.4
🔧 3.2 权限配置
在 OpenHarmony 平台上播放网络视频需要配置网络权限。
步骤一:配置权限声明
在 ohos/entry/src/main/module.json5 文件中添加权限声明:
{
"module": {
"name": "entry",
"type": "entry",
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:internet_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
步骤二:添加字符串资源
在 ohos/entry/src/main/resources/base/element/string.json 文件中添加权限说明字符串:
{
"string": [
{
"name": "internet_reason",
"value": "用于播放网络视频和加载视频资源"
}
]
}
📁 3.3 Asset 资源配置(可选)
如果需要使用 Asset 视频资源,需要在 pubspec.yaml 中声明资源文件:
flutter:
assets:
- assets/videos/
🛠️ 四、核心服务实现
🎬 4.1 视频播放服务
首先,我们实现一个视频播放服务,封装 video_player 的底层 API:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
/// 视频播放服务
///
/// 该服务封装了 video_player 的底层 API,提供统一的视频播放接口。
/// 所有方法都是实例方法,通过单例模式访问。
class VideoPlayerService {
/// 单例实例
static final VideoPlayerService _instance = VideoPlayerService._internal();
/// 获取单例实例
static VideoPlayerService get instance => _instance;
/// 私有构造函数
VideoPlayerService._internal();
/// 当前视频控制器
VideoPlayerController? _controller;
/// 获取当前控制器
VideoPlayerController? get controller => _controller;
/// 是否已初始化
bool get isInitialized => _controller?.value.isInitialized ?? false;
/// 是否正在播放
bool get isPlaying => _controller?.value.isPlaying ?? false;
/// 是否正在缓冲
bool get isBuffering => _controller?.value.isBuffering ?? false;
/// 是否循环播放
bool get isLooping => _controller?.value.isLooping ?? false;
/// 获取视频时长
Duration get duration => _controller?.value.duration ?? Duration.zero;
/// 获取当前播放位置
Duration get position => _controller?.value.position ?? Duration.zero;
/// 获取视频宽高比
double get aspectRatio => _controller?.value.aspectRatio ?? 16 / 9;
/// 获取播放速度
double get playbackSpeed => _controller?.value.playbackSpeed ?? 1.0;
/// 获取音量
double get volume => _controller?.value.volume ?? 1.0;
/// 初始化网络视频
///
/// [url] 视频地址
/// [autoPlay] 是否自动播放
/// [looping] 是否循环播放
Future<bool> initializeNetworkVideo(
String url, {
bool autoPlay = false,
bool looping = false,
}) async {
await _disposeController();
try {
_controller = VideoPlayerController.networkUrl(Uri.parse(url));
await _controller!.initialize();
if (looping) {
_controller!.setLooping(true);
}
if (autoPlay) {
_controller!.play();
}
return true;
} catch (e) {
debugPrint('初始化网络视频失败: $e');
return false;
}
}
/// 初始化本地文件视频
///
/// [file] 视频文件
/// [autoPlay] 是否自动播放
/// [looping] 是否循环播放
Future<bool> initializeFileVideo(
File file, {
bool autoPlay = false,
bool looping = false,
}) async {
await _disposeController();
try {
_controller = VideoPlayerController.file(file);
await _controller!.initialize();
if (looping) {
_controller!.setLooping(true);
}
if (autoPlay) {
_controller!.play();
}
return true;
} catch (e) {
debugPrint('初始化本地视频失败: $e');
return false;
}
}
/// 初始化 Asset 视频
///
/// [assetPath] Asset 资源路径
/// [autoPlay] 是否自动播放
/// [looping] 是否循环播放
Future<bool> initializeAssetVideo(
String assetPath, {
bool autoPlay = false,
bool looping = false,
}) async {
await _disposeController();
try {
_controller = VideoPlayerController.asset(assetPath);
await _controller!.initialize();
if (looping) {
_controller!.setLooping(true);
}
if (autoPlay) {
_controller!.play();
}
return true;
} catch (e) {
debugPrint('初始化 Asset 视频失败: $e');
return false;
}
}
/// 播放
void play() {
_controller?.play();
}
/// 暂停
void pause() {
_controller?.pause();
}
/// 切换播放/暂停
void togglePlayPause() {
if (isPlaying) {
pause();
} else {
play();
}
}
/// 跳转到指定位置
///
/// [position] 目标位置
void seekTo(Duration position) {
_controller?.seekTo(position);
}
/// 跳转到进度位置
///
/// [progress] 进度值 (0.0 - 1.0)
void seekToProgress(double progress) {
final targetPosition = Duration(
milliseconds: (duration.inMilliseconds * progress).round(),
);
seekTo(targetPosition);
}
/// 快进
///
/// [seconds] 快进秒数
void fastForward(int seconds) {
final newPosition = position + Duration(seconds: seconds);
if (newPosition < duration) {
seekTo(newPosition);
} else {
seekTo(duration);
}
}
/// 快退
///
/// [seconds] 快退秒数
void rewind(int seconds) {
final newPosition = position - Duration(seconds: seconds);
if (newPosition > Duration.zero) {
seekTo(newPosition);
} else {
seekTo(Duration.zero);
}
}
/// 设置播放速度
///
/// [speed] 播放速度 (0.25, 0.5, 1.0, 1.5, 2.0 等)
void setPlaybackSpeed(double speed) {
_controller?.setPlaybackSpeed(speed);
}
/// 设置循环播放
///
/// [looping] 是否循环
void setLooping(bool looping) {
_controller?.setLooping(looping);
}
/// 设置音量
///
/// [volume] 音量 (0.0 - 1.0)
void setVolume(double volume) {
_controller?.setVolume(volume.clamp(0.0, 1.0));
}
/// 添加状态监听器
///
/// [listener] 监听回调
void addListener(VoidCallback listener) {
_controller?.addListener(listener);
}
/// 移除状态监听器
///
/// [listener] 监听回调
void removeListener(VoidCallback listener) {
_controller?.removeListener(listener);
}
/// 获取播放进度信息
PlaybackProgress getProgress() {
return PlaybackProgress(
position: position,
duration: duration,
buffered: Duration.zero,
speed: playbackSpeed,
);
}
/// 释放控制器
Future<void> _disposeController() async {
await _controller?.dispose();
_controller = null;
}
/// 释放资源
Future<void> dispose() async {
await _disposeController();
}
}
📋 4.2 播放进度信息类
/// 播放进度信息
class PlaybackProgress {
final Duration position;
final Duration duration;
final Duration buffered;
final double speed;
const PlaybackProgress({
required this.position,
required this.duration,
required this.buffered,
this.speed = 1.0,
});
double get progress {
if (duration.inMilliseconds == 0) return 0;
return position.inMilliseconds / duration.inMilliseconds;
}
double get bufferedProgress {
if (duration.inMilliseconds == 0) return 0;
return buffered.inMilliseconds / duration.inMilliseconds;
}
String get formattedPosition => _formatDuration(position);
String get formattedDuration => _formatDuration(duration);
static String _formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
if (hours > 0) {
return '${hours.toString().padLeft(2, '0')}:'
'${minutes.toString().padLeft(2, '0')}:'
'${seconds.toString().padLeft(2, '0')}';
}
return '${minutes.toString().padLeft(2, '0')}:'
'${seconds.toString().padLeft(2, '0')}';
}
}
📝 五、完整示例代码
下面是一个完整的专业级视频播放系统示例:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
// ============ 数据模型 ============
enum VideoSourceType { network, file, asset }
enum VideoPlayState { idle, loading, ready, playing, paused, buffering, error, completed }
class VideoInfo {
final String title;
final String? description;
final String source;
final VideoSourceType sourceType;
final String? thumbnail;
const VideoInfo({
required this.title,
this.description,
required this.source,
required this.sourceType,
this.thumbnail,
});
}
class PlaybackProgress {
final Duration position;
final Duration duration;
final double speed;
const PlaybackProgress({
required this.position,
required this.duration,
this.speed = 1.0,
});
double get progress {
if (duration.inMilliseconds == 0) return 0;
return position.inMilliseconds / duration.inMilliseconds;
}
String get formattedPosition => _formatDuration(position);
String get formattedDuration => _formatDuration(duration);
static String _formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
if (hours > 0) {
return '${hours.toString().padLeft(2, '0')}:'
'${minutes.toString().padLeft(2, '0')}:'
'${seconds.toString().padLeft(2, '0')}';
}
return '${minutes.toString().padLeft(2, '0')}:'
'${seconds.toString().padLeft(2, '0')}';
}
}
// ============ 服务类 ============
class VideoPlayerService {
static final VideoPlayerService _instance = VideoPlayerService._internal();
static VideoPlayerService get instance => _instance;
VideoPlayerService._internal();
VideoPlayerController? _controller;
VideoPlayerController? get controller => _controller;
bool get isInitialized => _controller?.value.isInitialized ?? false;
bool get isPlaying => _controller?.value.isPlaying ?? false;
bool get isBuffering => _controller?.value.isBuffering ?? false;
Duration get duration => _controller?.value.duration ?? Duration.zero;
Duration get position => _controller?.value.position ?? Duration.zero;
double get aspectRatio => _controller?.value.aspectRatio ?? 16 / 9;
double get playbackSpeed => _controller?.value.playbackSpeed ?? 1.0;
Future<bool> initializeNetworkVideo(
String url, {
bool autoPlay = false,
bool looping = false,
}) async {
await _disposeController();
try {
_controller = VideoPlayerController.networkUrl(Uri.parse(url));
await _controller!.initialize();
if (looping) _controller!.setLooping(true);
if (autoPlay) _controller!.play();
return true;
} catch (e) {
debugPrint('初始化网络视频失败: $e');
return false;
}
}
Future<bool> initializeFileVideo(
File file, {
bool autoPlay = false,
bool looping = false,
}) async {
await _disposeController();
try {
_controller = VideoPlayerController.file(file);
await _controller!.initialize();
if (looping) _controller!.setLooping(true);
if (autoPlay) _controller!.play();
return true;
} catch (e) {
debugPrint('初始化本地视频失败: $e');
return false;
}
}
Future<bool> initializeAssetVideo(
String assetPath, {
bool autoPlay = false,
bool looping = false,
}) async {
await _disposeController();
try {
_controller = VideoPlayerController.asset(assetPath);
await _controller!.initialize();
if (looping) _controller!.setLooping(true);
if (autoPlay) _controller!.play();
return true;
} catch (e) {
debugPrint('初始化 Asset 视频失败: $e');
return false;
}
}
void play() => _controller?.play();
void pause() => _controller?.pause();
void togglePlayPause() {
if (isPlaying) {
pause();
} else {
play();
}
}
void seekTo(Duration position) => _controller?.seekTo(position);
void seekToProgress(double progress) {
final targetPosition = Duration(
milliseconds: (duration.inMilliseconds * progress).round(),
);
seekTo(targetPosition);
}
void fastForward(int seconds) {
final newPosition = position + Duration(seconds: seconds);
seekTo(newPosition < duration ? newPosition : duration);
}
void rewind(int seconds) {
final newPosition = position - Duration(seconds: seconds);
seekTo(newPosition > Duration.zero ? newPosition : Duration.zero);
}
void setPlaybackSpeed(double speed) => _controller?.setPlaybackSpeed(speed);
void setLooping(bool looping) => _controller?.setLooping(looping);
void setVolume(double volume) => _controller?.setVolume(volume.clamp(0.0, 1.0));
void addListener(VoidCallback listener) => _controller?.addListener(listener);
void removeListener(VoidCallback listener) => _controller?.removeListener(listener);
PlaybackProgress getProgress() {
return PlaybackProgress(
position: position,
duration: duration,
speed: playbackSpeed,
);
}
Future<void> _disposeController() async {
await _controller?.dispose();
_controller = null;
}
Future<void> dispose() async {
await _disposeController();
}
}
// ============ 应用入口 ============
void main() {
runApp(const VideoPlayerApp());
}
class VideoPlayerApp extends StatelessWidget {
const VideoPlayerApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '视频播放系统',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
useMaterial3: true,
),
home: const VideoListPage(),
);
}
}
class VideoListPage extends StatelessWidget {
const VideoListPage({super.key});
static final List<VideoInfo> _videos = [
VideoInfo(
title: '示例视频 1',
description: '这是一个示例网络视频',
source: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
sourceType: VideoSourceType.network,
),
VideoInfo(
title: '示例视频 2',
description: '这是另一个示例网络视频',
source: 'https://media.w3.org/2010/05/bunny/trailer.mp4',
sourceType: VideoSourceType.network,
),
];
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('视频播放系统'),
centerTitle: true,
),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _videos.length,
itemBuilder: (context, index) {
final video = _videos[index];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: Container(
width: 80,
height: 60,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.play_circle_outline, size: 32),
),
title: Text(
video.title,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
video.description ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => VideoPlayerPage(video: video),
),
);
},
),
);
},
),
);
}
}
class VideoPlayerPage extends StatefulWidget {
final VideoInfo video;
const VideoPlayerPage({super.key, required this.video});
State<VideoPlayerPage> createState() => _VideoPlayerPageState();
}
class _VideoPlayerPageState extends State<VideoPlayerPage> {
final VideoPlayerService _service = VideoPlayerService.instance;
VideoPlayState _playState = VideoPlayState.idle;
bool _showControls = true;
String? _errorMessage;
final List<double> _speedOptions = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];
int _currentSpeedIndex = 2;
void initState() {
super.initState();
_initializeVideo();
}
Future<void> _initializeVideo() async {
setState(() => _playState = VideoPlayState.loading);
bool success = false;
switch (widget.video.sourceType) {
case VideoSourceType.network:
success = await _service.initializeNetworkVideo(widget.video.source);
break;
case VideoSourceType.file:
success = await _service.initializeFileVideo(File(widget.video.source));
break;
case VideoSourceType.asset:
success = await _service.initializeAssetVideo(widget.video.source);
break;
}
if (!mounted) return;
if (success) {
setState(() => _playState = VideoPlayState.ready);
_service.addListener(_onVideoStateChanged);
} else {
setState(() {
_playState = VideoPlayState.error;
_errorMessage = '视频加载失败';
});
}
}
void _onVideoStateChanged() {
if (!mounted) return;
final controller = _service.controller;
if (controller == null) return;
if (controller.value.isBuffering) {
setState(() => _playState = VideoPlayState.buffering);
} else if (controller.value.isPlaying) {
setState(() => _playState = VideoPlayState.playing);
} else if (controller.value.position >= controller.value.duration) {
setState(() => _playState = VideoPlayState.completed);
} else {
setState(() => _playState = VideoPlayState.paused);
}
}
void dispose() {
_service.removeListener(_onVideoStateChanged);
_service.pause();
super.dispose();
}
void _toggleControls() {
setState(() => _showControls = !_showControls);
}
void _togglePlayPause() {
_service.togglePlayPause();
}
void _seekTo(double progress) {
_service.seekToProgress(progress);
}
void _changeSpeed() {
setState(() {
_currentSpeedIndex = (_currentSpeedIndex + 1) % _speedOptions.length;
_service.setPlaybackSpeed(_speedOptions[_currentSpeedIndex]);
});
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: Text(widget.video.title),
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
body: Column(
children: [
Expanded(
child: _buildVideoPlayer(),
),
if (_showControls && _service.isInitialized) _buildControlPanel(),
],
),
);
}
Widget _buildVideoPlayer() {
if (_playState == VideoPlayState.loading) {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: Colors.white),
SizedBox(height: 16),
Text('加载中...', style: TextStyle(color: Colors.white)),
],
),
);
}
if (_playState == VideoPlayState.error) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
_errorMessage ?? '加载失败',
style: const TextStyle(color: Colors.white),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _initializeVideo,
child: const Text('重试'),
),
],
),
);
}
if (!_service.isInitialized) {
return const Center(
child: Text('视频未初始化', style: TextStyle(color: Colors.white)),
);
}
return GestureDetector(
onTap: _toggleControls,
child: Stack(
alignment: Alignment.center,
children: [
Center(
child: AspectRatio(
aspectRatio: _service.aspectRatio,
child: VideoPlayer(_service.controller!),
),
),
if (_showControls) _buildOverlayControls(),
if (_playState == VideoPlayState.buffering)
const CircularProgressIndicator(color: Colors.white),
],
),
);
}
Widget _buildOverlayControls() {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.3),
Colors.transparent,
Colors.black.withOpacity(0.3),
],
),
),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
iconSize: 40,
icon: const Icon(Icons.replay_10, color: Colors.white),
onPressed: () => _service.rewind(10),
),
const SizedBox(width: 32),
GestureDetector(
onTap: _togglePlayPause,
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: Icon(
_service.isPlaying ? Icons.pause : Icons.play_arrow,
size: 48,
color: Colors.white,
),
),
),
const SizedBox(width: 32),
IconButton(
iconSize: 40,
icon: const Icon(Icons.forward_10, color: Colors.white),
onPressed: () => _service.fastForward(10),
),
],
),
),
);
}
Widget _buildControlPanel() {
final progress = _service.getProgress();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade900,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(
progress.formattedPosition,
style: const TextStyle(color: Colors.white, fontSize: 12),
),
Expanded(
child: Slider(
value: progress.progress.clamp(0.0, 1.0),
onChanged: _seekTo,
activeColor: Colors.red,
inactiveColor: Colors.grey.shade700,
),
),
Text(
progress.formattedDuration,
style: const TextStyle(color: Colors.white, fontSize: 12),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.replay_10, color: Colors.white),
onPressed: () => _service.rewind(10),
),
IconButton(
icon: Icon(
_service.isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 32,
),
onPressed: _togglePlayPause,
),
IconButton(
icon: const Icon(Icons.forward_10, color: Colors.white),
onPressed: () => _service.fastForward(10),
),
TextButton(
onPressed: _changeSpeed,
child: Text(
'${_speedOptions[_currentSpeedIndex]}x',
style: const TextStyle(color: Colors.white),
),
),
],
),
],
),
);
}
}
🏆 六、最佳实践与注意事项
⚠️ 6.1 资源管理最佳实践
及时释放资源:视频控制器占用大量系统资源,务必在组件销毁时调用 dispose() 方法释放资源。
避免重复初始化:在初始化新视频之前,先释放之前的控制器,避免内存泄漏。
使用单例模式:对于全局视频播放服务,使用单例模式确保只有一个控制器实例。
🔐 6.2 网络视频注意事项
HTTPS 支持:建议使用 HTTPS 协议的视频源,确保传输安全。
错误处理:网络视频可能因网络问题加载失败,需要提供重试机制。
缓存策略:对于频繁播放的视频,考虑实现本地缓存机制。
📱 6.3 OpenHarmony 平台特殊说明
播放器后端:OpenHarmony 使用 AVPlayer 作为底层播放器。
格式支持:支持 MP4、MKV、WebM 等常见视频格式。
权限配置:播放网络视频需要配置 INTERNET 权限。
📌 七、总结
本文通过一个完整的专业级视频播放系统案例,深入讲解了 video_player 第三方库的使用方法与最佳实践:
架构设计:采用分层架构(UI层 → 服务层 → 基础设施层),让代码更清晰,便于维护和测试。
服务封装:统一封装视频播放逻辑,提供语义化的方法名,让调用代码更易读。
状态管理:完善的播放状态管理机制,支持多种播放状态和状态转换。
自定义界面:灵活的控制面板设计,支持自定义播放器 UI。
掌握这些技巧,你就能构建出专业级的视频播放功能,为用户提供流畅、可靠的视频观看体验。
参考资料
更多推荐



所有评论(0)