Flutter for OpenHarmony第三方库实战:视频内容创作工具箱 —— video_player + video_thumbnail 组合应用
想象一下这样的场景:用户打开你的应用,选择一个视频进行预览,生成视频缩略图,然后分享视频。这个流程涵盖了视频内容创作的核心环节。fill:#333;important;important;fill:none;color:#333;color:#333;important;fill:none;fill:#333;height:1em;视频选择视频播放预览生成缩略图分享导出通过本文的学习,我们成功构建

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🚀 项目概述:我们要构建什么?
想象一下这样的场景:用户打开你的应用,选择一个视频进行预览,生成视频缩略图,然后分享视频。这个流程涵盖了视频内容创作的核心环节。
🎯 核心功能一览
| 功能模块 | 实现库 | 核心能力 |
|---|---|---|
| 📹 视频播放 | video_player | 视频播放、控制、进度管理 |
| 🖼️ 缩略图生成 | video_thumbnail | 快速生成视频缩略图 |
💡 为什么选择这两个库?
1️⃣ video_player - 官方维护,功能完备
- Flutter 官方团队维护,质量有保障
- 支持多种视频格式和来源
- 提供完整的播放控制 API
2️⃣ video_thumbnail - 高效生成,支持多种格式
- 快速生成视频缩略图
- 支持多种缩略图格式(JPEG、PNG、WEBP)
- 支持指定时间点生成缩略图
📦 第一步:环境配置
1.1 添加依赖
打开 pubspec.yaml,添加两个库的依赖:
dependencies:
flutter:
sdk: flutter
# 视频播放
video_player:
git:
url: https://gitcode.com/openharmony-tpc/flutter_packages.git
path: packages/video_player
# 视频缩略图
video_thumbnail:
git:
url: https://atomgit.com/openharmony-sig/fluttertpc_video_thumbnail.git
ref: master
# 视频缩略图 OpenHarmony 版本
video_thumbnail_ohos:
git:
url: https://atomgit.com/openharmony-sig/fluttertpc_video_thumbnail.git
path: ohos
dev_dependencies:
# 视频播放鸿蒙平台支持
video_player_ohos:
git:
url: https://gitcode.com/openharmony-tpc/flutter_packages.git
path: packages/video_player/video_player_ohos
⚠️ 注意:
video_player_ohos需要作为dev_dependency引入,这是鸿蒙平台的原生实现。
1.2 权限配置
在 OpenHarmony 平台上,需要配置相关权限:
📄 ohos/entry/src/main/module.json5:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:network_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.READ_MEDIA",
"reason": "$string:read_media_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
1.3 执行依赖安装
flutter pub get
📹 第二步:视频播放模块
2.1 理解 VideoPlayer 的核心概念
VideoPlayer 是 Flutter 官方提供的视频播放插件,它的设计理念是简洁而强大。通过控制器模式,提供了完整的视频播放控制能力。
// 创建视频控制器
final _controller = VideoPlayerController.file(File(videoPath))
..initialize().then((_) {
// 视频初始化完成
setState(() {});
});
// 播放视频
_controller.play();
// 暂停视频
_controller.pause();
// 跳转到指定位置
_controller.seekTo(Duration(seconds: 10));
2.2 视频状态管理
VideoPlayerController 提供了丰富的状态信息:
// 监听视频播放状态
_controller.addListener(() {
setState(() {
// 获取当前播放位置
final position = _controller.value.position;
// 获取视频总时长
final duration = _controller.value.duration;
// 获取播放状态
final isPlaying = _controller.value.isPlaying;
// 获取初始化状态
final isInitialized = _controller.value.isInitialized;
});
});
2.3 视频播放 UI 集成
使用 VideoPlayer Widget 构建视频播放界面:
VideoPlayer(_controller)
🖼️ 第三步:缩略图生成模块
3.1 缩略图生成原理
video_thumbnail 库通过解析视频文件,提取指定时间点的帧作为缩略图:
// 生成视频缩略图
final thumbnailPath = await VideoThumbnailOhos.thumbnailFile(
video: videoPath,
thumbnailPath: (await getTemporaryDirectory()).path,
imageFormat: ThumbnailFormat.JPEG,
maxWidth: 200, // 最大宽度
quality: 75, // 图片质量
);
3.2 缩略图格式选择
库支持三种缩略图格式:
| 格式 | 特点 | 适用场景 |
|---|---|---|
| JPEG | 压缩率高,文件小 | 网络传输 |
| PNG | 无损压缩,质量好 | 本地存储 |
| WEBP | 现代格式,平衡大小和质量 | 移动应用 |
3.3 批量生成缩略图
可以为视频的不同时间点生成缩略图,用于创建视频预览:
// 为视频的不同时间点生成缩略图
List<String> thumbnails = [];
for (int i = 0; i < 5; i++) {
final thumbnailPath = await VideoThumbnailOhos.thumbnailFile(
video: videoPath,
timeMs: (duration.inMilliseconds * i / 4).round(),
// 其他参数...
);
thumbnails.add(thumbnailPath!);
}
🎨 第四步:完整应用实现
现在,让我们把所有模块组合起来,构建一个完整的视频内容创作工具箱应用:
4.1 应用架构设计
4.2 完整代码实现
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:video_player/video_player.dart';
import 'package:video_thumbnail_ohos/video_thumbnail_ohos.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_extend/share_extend.dart';
void main() {
runApp(const VideoToolboxApp());
}
class VideoToolboxApp extends StatelessWidget {
const VideoToolboxApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '视频内容创作工具箱',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF10B981)),
useMaterial3: true,
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
),
home: const VideoToolboxPage(),
);
}
}
class VideoToolboxPage extends StatefulWidget {
const VideoToolboxPage({super.key});
State<VideoToolboxPage> createState() => _VideoToolboxPageState();
}
class _VideoToolboxPageState extends State<VideoToolboxPage> {
final ImagePicker _picker = ImagePicker();
VideoPlayerController? _controller;
File? _selectedVideo;
List<String> _thumbnails = [];
bool _isProcessing = false;
String _statusMessage = '';
void dispose() {
_controller?.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
title: const Text('视频内容创作工具箱'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: _buildBody(),
bottomNavigationBar: _buildBottomBar(),
);
}
Widget _buildBody() {
if (_selectedVideo == null) {
return _buildEmptyState();
}
if (_isProcessing) {
return _buildProcessingState();
}
return SingleChildScrollView(
child: Column(
children: [
_buildVideoPlayer(),
_buildThumbnailSection(),
_buildActionButtons(),
],
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.video_library_outlined,
size: 80,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 24),
Text(
'开始你的视频创作之旅',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'选择视频 → 生成缩略图 → 分享',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
const SizedBox(height: 32),
FilledButton.icon(
onPressed: _pickVideo,
icon: const Icon(Icons.video_file),
label: const Text('选择视频'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
),
),
],
),
);
}
Widget _buildProcessingState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 24),
Text(
_statusMessage.isNotEmpty ? _statusMessage : '正在处理视频...',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
);
}
Widget _buildVideoPlayer() {
if (_controller == null) return const SizedBox.shrink();
return Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
VideoPlayer(_controller!),
VideoProgressIndicator(_controller!, allowScrubbing: true),
],
),
),
);
}
Widget _buildThumbnailSection() {
if (_thumbnails.isEmpty) return const SizedBox.shrink();
return Container(
margin: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'视频缩略图',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _thumbnails.length,
itemBuilder: (context, index) {
return Container(
margin: const EdgeInsets.only(right: 8),
width: 150,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.grey[200],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
children: [
Image.file(
File(_thumbnails[index]),
fit: BoxFit.cover,
width: 150,
height: 100,
),
Positioned(
bottom: 4,
right: 4,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'${(index * 2)}s',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
),
),
],
),
),
);
},
),
),
],
),
);
}
Widget _buildActionButtons() {
return Container(
margin: const EdgeInsets.all(16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildActionButton(
icon: Icons.image_outlined,
label: '生成缩略图',
onTap: _generateThumbnails,
),
_buildActionButton(
icon: Icons.share_outlined,
label: '分享视频',
onTap: _shareVideo,
),
],
),
const SizedBox(height: 16),
if (_controller != null) ...[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {
if (_controller!.value.isPlaying) {
_controller!.pause();
} else {
_controller!.play();
}
},
icon: Icon(
_controller!.value.isPlaying ? Icons.pause : Icons.play_arrow,
size: 40,
),
),
],
),
],
],
),
);
}
Widget _buildActionButton({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: Theme.of(context).colorScheme.primary, size: 32),
const SizedBox(height: 8),
Text(
label,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 14,
),
),
],
),
),
);
}
Widget _buildBottomBar() {
if (_selectedVideo == null) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -5),
),
],
),
child: SafeArea(
child: Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _pickVideo,
icon: const Icon(Icons.add),
label: const Text('选择视频'),
),
),
const SizedBox(width: 16),
Expanded(
child: FilledButton.icon(
onPressed: _clearAll,
icon: const Icon(Icons.clear_all),
label: const Text('清空'),
),
),
],
),
),
);
}
Future<void> _pickVideo() async {
final XFile? video = await _picker.pickVideo(
source: ImageSource.gallery,
maxDuration: const Duration(minutes: 10),
);
if (video != null) {
setState(() {
_selectedVideo = File(video.path);
_thumbnails = [];
});
// 初始化视频控制器
_controller = VideoPlayerController.file(_selectedVideo!)
..initialize().then((_) {
setState(() {});
});
}
}
Future<void> _generateThumbnails() async {
if (_selectedVideo == null) return;
setState(() {
_isProcessing = true;
_statusMessage = '正在生成缩略图...';
});
try {
final tempDir = await getTemporaryDirectory();
final List<String> thumbnails = [];
final sessionDir = Directory('${tempDir.path}/thumbnails_${DateTime.now().millisecondsSinceEpoch}');
if (!await sessionDir.exists()) {
await sessionDir.create(recursive: true);
}
for (int i = 0; i < 5; i++) {
final bytes = await VideoThumbnailOhos.thumbnailData(
video: _selectedVideo!.path,
imageFormat: ImageFormat.JPEG,
maxHeight: 200,
maxWidth: 200,
timeMs: (i * 2000),
quality: 75,
);
if (bytes != null) {
final file = File('${sessionDir.path}/thumb_$i.jpg');
await file.writeAsBytes(bytes);
thumbnails.add(file.path);
}
}
setState(() {
_thumbnails = thumbnails;
_isProcessing = false;
_statusMessage = '';
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('缩略图生成完成!')),
);
}
} catch (e) {
setState(() {
_isProcessing = false;
_statusMessage = '';
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('生成缩略图失败: $e')),
);
}
}
}
Future<void> _shareVideo() async {
if (_selectedVideo == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('没有可分享的视频')),
);
}
return;
}
try {
await ShareExtend.share(_selectedVideo!.path, "file");
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('分享失败: $e')),
);
}
}
}
void _clearAll() {
_controller?.dispose();
setState(() {
_selectedVideo = null;
_controller = null;
_thumbnails = [];
_statusMessage = '';
});
}
}
🎯 第五步:功能详解与优化
5.1 工作流状态管理
我们的应用采用了清晰的状态管理:
// 核心状态变量
VideoPlayerController? _controller; // 视频控制器
File? _selectedVideo; // 选中的视频文件
List<String> _thumbnails = []; // 生成的缩略图列表
bool _isProcessing = false; // 处理中状态
String _statusMessage = ''; // 状态消息
5.2 用户体验优化
🔄 加载状态反馈
if (_isProcessing) {
return _buildProcessingState(); // 显示加载动画和进度
}
📱 视频播放控制
使用 VideoProgressIndicator 提供直观的播放控制:
VideoProgressIndicator(_controller!, allowScrubbing: true)
5.3 错误处理策略
try {
// 执行可能失败的操作
await someOperation();
} catch (e) {
// 友好的错误提示
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('操作失败: $e')),
);
}
}
💡 扩展思路
🎨 添加视频编辑功能
可以集成 video_editor 库,添加视频剪辑、添加音乐等功能:
// 视频编辑示例
final editor = VideoEditor();
final editedVideo = await editor.edit(videoPath);
📊 添加视频信息分析
可以添加视频信息分析功能,显示视频的详细信息:
// 获取视频信息
final info = await VideoInfo.get(videoPath);
print('分辨率: ${info.width}x${info.height}');
print('时长: ${info.duration}');
print('帧率: ${info.fps}');
🎬 添加视频滤镜功能
可以集成视频滤镜库,为视频添加各种滤镜效果:
// 应用视频滤镜
final filteredVideo = await VideoFilter.apply(
videoPath,
filter: VideoFilter.beautify,
);
📝 总结
通过本文的学习,我们成功构建了一个完整的视频内容创作工具箱应用,掌握了以下核心技能:
✅ 视频播放:使用 video_player 实现视频的播放、暂停、跳转等功能
✅ 缩略图生成:使用 video_thumbnail 快速生成视频缩略图
✅ 状态管理:清晰的状态管理和用户反馈机制
✅ 错误处理:完善的错误处理和用户提示
这个应用为用户提供了完整的视频内容创作工具,从视频选择、预览、缩略图生成到分享,一气呵成!
更多推荐
所有评论(0)