
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🎯 欢迎来到 Flutter for OpenHarmony 社区!本文将深入讲解 Flutter 中 video_thumbnail 视频缩略图插件的使用方法,带你全面掌握从视频中提取缩略图、设置缩略图参数等功能。
一、video_thumbnail 组件概述
在 Flutter for OpenHarmony 应用开发中,video_thumbnail 是一个非常实用的插件,用于从视频文件中提取缩略图。它支持本地视频和网络视频,可以自定义缩略图的尺寸、格式、质量和提取时间点。
📋 video_thumbnail 组件特点
| 特点 | 说明 |
|---|---|
| 跨平台支持 | 支持 Android、iOS、OpenHarmony |
| 多种来源 | 支持本地视频文件和网络视频 URL |
| 多种格式 | 支持 JPEG、PNG、WebP 输出格式 |
| 尺寸控制 | 支持设置最大宽高限制 |
| 时间点选择 | 支持指定提取缩略图的时间点 |
| 质量控制 | 支持设置输出图片质量 |
| 两种输出方式 | 支持返回文件路径或内存数据 |
| HTTP 头支持 | 支持自定义 HTTP 请求头 |
💡 使用场景:视频列表预览、视频编辑应用、媒体库管理、视频分享预览等需要显示视频缩略图的场景。
二、OpenHarmony 平台适配说明
2.1 兼容性信息
本项目适配 Flutter 3.7.12-ohos-1.0.6,SDK 5.0.0(12),DevEco Studio 5.0.13.200,ROM 5.1.0.120 SP3。
2.2 支持的功能
在 OpenHarmony 平台上,video_thumbnail 支持以下功能:
| 功能 | 说明 | OpenHarmony 支持 |
|---|---|---|
| thumbnailData() | 生成缩略图数据(内存) | ✅ yes |
| thumbnailFile() | 生成缩略图文件 | ✅ yes |
| JPEG 格式 | ImageFormat.JPEG | ✅ yes |
| PNG 格式 | ImageFormat.PNG | ✅ yes |
| WebP 格式 | ImageFormat.WEBP | ✅ yes |
| 自定义尺寸 | maxHeight / maxWidth | ✅ yes |
| 自定义质量 | quality (0-100) | ✅ yes |
| 时间点选择 | timeMs | ✅ yes |
| HTTP 头 | headers | ✅ yes |
| 本地视频 | 本地文件路径 | ✅ yes |
| 网络视频 | URL 地址 | ✅ yes (API>=20) |
⚠️ 注意:网络视频需要 SDK API 版本 >= 20 才支持。如果 API 版本低于 20,会抛出错误 "Below API20 and an online url, thumbnail retrieval is not supported."
三、项目配置与安装
3.1 添加依赖配置
首先,需要在你的 Flutter 项目的 pubspec.yaml 文件中添加 video_thumbnail_ohos 依赖。
打开项目根目录下的 pubspec.yaml 文件,找到 dependencies 部分,添加以下配置:
yaml
dependencies:
flutter:
sdk: flutter
# 添加 video_thumbnail 依赖(OpenHarmony 适配版本)
video_thumbnail_ohos:
git:
url: https://atomgit.com/openharmony-sig/fluttertpc_video_thumbnail.git
path: ohos
配置说明:
- 使用 git 方式引用开源鸿蒙适配的 fluttertpc_video_thumbnail 仓库
url:指定 AtomGit托管的仓库地址path:指定 ohos 包的具体路径- 本插件直接使用
video_thumbnail_ohos包,无需额外的 dev_dependency
⚠️ 重要 :对于 OpenHarmony 平台,直接使用
video_thumbnail_ohos包即可,这是鸿蒙平台的原生实现。
3.2 下载依赖
配置完成后,需要在项目根目录执行以下命令下载依赖:
bash
flutter pub get
执行成功后,你会看到类似以下的输出:
Running "flutter pub get" in my_cross_platform_app...
Resolving dependencies...
Got dependencies!
3.3 权限配置
在 OpenHarmony 平台上,使用 video_thumbnail 需要配置读取媒体文件的权限。
ohos/entry/src/main/module.json5:
json
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.READ_IMAGEVIDEO",
"reason": "$string:read_media_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
ohos/entry/src/main/resources/base/element/string.json:
添加权限申请原因:
json
{
"string": [
{
"name": "read_media_reason",
"value": "读取视频文件生成缩略图"
}
]
}
3.4 权限等级说明
在 OpenHarmony 系统中,权限分为三个等级:
| 等级 | 说明 |
|---|---|
| normal | 普通权限,普通应用可直接申请 |
| system_basic | 系统基础权限,需要系统签名或特殊配置 |
| system_core | 系统核心权限,仅系统应用可使用 |
重要提示 :ohos.permission.READ_IMAGEVIDEO 属于 system_basic 级别权限,默认的应用权限等级是 normal。如果直接安装包含此权限的应用,会报错 9568289。
3.5 如何使用 system_basic 权限
要使用 READ_IMAGEVIDEO 权限,需要修改应用的权限等级。请参考华为官方文档:安装 HAP 时提示 code 9568289
步骤 1:修改 Debug 签名模板
找到 SDK 目录下的签名模板文件:
{SDK路径}/openharmony/toolchains/lib/UnsgnedDebugProfileTemplate.json
例如:d:\huawei\DevEco Studio\sdk\default\openharmony\toolchains\lib\UnsgnedDebugProfileTemplate.json
打开文件,修改以下内容:
1. 确保 APL 等级为 system_basic:
json
"bundle-info": {
...
"apl": "system_basic",
...
}
2. 在 acls 中添加允许的权限:
json
"acls": {
"allowed-acls": [
"ohos.permission.READ_IMAGEVIDEO"
]
}
3. 在 permissions 中添加受限权限:
json
"permissions": {
"restricted-permissions": [
"ohos.permission.READ_IMAGEVIDEO"
]
}
完整的修改后的文件示例:
json
{
"version-name": "2.0.0",
"version-code": 2,
"uuid": "fe686e1b-3770-4824-a938-961b140a7c98",
"validity": {
"not-before": 1610519532,
"not-after": 1705127532
},
"type": "debug",
"bundle-info": {
"developer-id": "OpenHarmony",
"development-certificate": "...",
"bundle-name": "com.OpenHarmony.app.test",
"apl": "system_basic",
"app-feature": "hos_normal_app"
},
"acls": {
"allowed-acls": [
"ohos.permission.READ_IMAGEVIDEO"
]
},
"permissions": {
"restricted-permissions": [
"ohos.permission.READ_IMAGEVIDEO"
]
},
"debug-info": {
"device-ids": [...],
"device-id-type": "udid"
},
"issuer": "pki_internal"
}
步骤 2:修改 Release 签名模板(可选)
如果需要发布应用,同样修改 UnsgnedReleasedProfileTemplate.json 文件。
步骤 3:在 DevEco Studio 中重新签名
- 打开 DevEco Studio
- 点击 File > Project Structure > Project > Signing Configs
- 取消勾选 "Automatically generate signature"
- 重新勾选 "Automatically generate signature"
- 等待自动签名完成
- 点击 OK
步骤 4:重新运行应用
签名完成后,重新运行应用即可正常安装。
⚠️ 注意:修改签名模板后必须重新签名才能生效。如果仍然报错,请尝试清理项目后重新构建。
四、video_thumbnail 基础用法
4.1 导入库
在使用 video_thumbnail 之前,需要先导入库:
dart
import 'dart:typed_data';
import 'package:video_thumbnail_ohos/video_thumbnail_ohos.dart';
4.2 图片格式枚举
video_thumbnail 支持三种输出格式:
dart
enum ImageFormat { JPEG, PNG, WEBP }
| 格式 | 说明 | 特点 |
|---|---|---|
| JPEG | 有损压缩格式 | 文件小,适合照片类图像 |
| PNG | 无损压缩格式 | 支持透明,质量高 |
| WebP | 现代压缩格式 | 压缩率高,质量好 |
4.3 生成缩略图数据 - thumbnailData()
thumbnailData() 方法直接返回图片的二进制数据,适合直接在内存中使用:
dart
Future<Uint8List?> _generateThumbnailData(String videoPath) async {
final bytes = await VideoThumbnailOhos.thumbnailData(
video: videoPath,
imageFormat: ImageFormat.JPEG,
maxHeight: 256,
maxWidth: 256,
timeMs: 0,
quality: 50,
);
return bytes;
}
参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| video | String | ✅ | 视频文件路径或 URL |
| headers | Map<String, String>? | ❌ | HTTP 请求头(网络视频用) |
| imageFormat | ImageFormat | ❌ | 输出图片格式,默认 PNG |
| maxHeight | int | ❌ | 最大高度,建议设置具体值 |
| maxWidth | int | ❌ | 最大宽度,建议设置具体值 |
| timeMs | int | ❌ | 提取时间点(毫秒),默认 0 |
| quality | int | ❌ | 图片质量(0-100),PNG 忽略此参数 |
⚠️ 注意 :
maxHeight和maxWidth建议设置具体值(如 256),设为 0 可能导致FetchFrameByTime failed错误。
4.4 生成缩略图文件 - thumbnailFile()
thumbnailFile() 方法将缩略图保存为文件,返回文件路径:
dart
Future<String?> _generateThumbnailFile(String videoPath, String savePath) async {
final thumbnailPath = await VideoThumbnailOhos.thumbnailFile(
video: videoPath,
thumbnailPath: savePath,
imageFormat: ImageFormat.JPEG,
maxHeight: 256,
maxWidth: 256,
timeMs: 0,
quality: 50,
);
return thumbnailPath;
}
参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| video | String | ✅ | 视频文件路径或 URL |
| headers | Map<String, String>? | ❌ | HTTP 请求头(网络视频用) |
| thumbnailPath | String? | ❌ | 缩略图保存路径,null 则保存到视频同目录 |
| imageFormat | ImageFormat | ❌ | 输出图片格式,默认 PNG |
| maxHeight | int | ❌ | 最大高度,建议设置具体值 |
| maxWidth | int | ❌ | 最大宽度,建议设置具体值 |
| timeMs | int | ❌ | 提取时间点(毫秒),默认 0 |
| quality | int | ❌ | 图片质量(0-100),PNG 忽略此参数 |
4.5 显示缩略图
生成缩略图数据后,可以直接使用 Image.memory() 显示:
dart
Widget _buildThumbnail(Uint8List? thumbnailData) {
if (thumbnailData == null) {
return const Icon(Icons.video_library, size: 100);
}
return Image.memory(
thumbnailData,
fit: BoxFit.cover,
);
}
五、完整示例:视频缩略图生成器
下面是一个完整的示例,展示如何实现视频缩略图生成功能:
dart
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:video_thumbnail_ohos/video_thumbnail_ohos.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Video Thumbnail Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String? _videoPath;
Uint8List? _thumbnailData;
String? _thumbnailPath;
bool _isLoading = false;
String? _error;
ImageFormat _format = ImageFormat.JPEG;
int _quality = 50;
int _maxHeight = 256;
int _maxWidth = 256;
int _timeMs = 0;
Future<void> _pickVideo() async {
final picker = ImagePicker();
final video = await picker.pickVideo(source: ImageSource.gallery);
if (video != null) {
setState(() {
_videoPath = video.path;
_thumbnailData = null;
_thumbnailPath = null;
_error = null;
});
}
}
Future<void> _generateThumbnailData() async {
if (_videoPath == null) return;
setState(() {
_isLoading = true;
_error = null;
});
try {
final bytes = await VideoThumbnailOhos.thumbnailData(
video: _videoPath!,
imageFormat: _format,
maxHeight: _maxHeight,
maxWidth: _maxWidth,
timeMs: _timeMs,
quality: _quality,
);
setState(() {
_thumbnailData = bytes;
_thumbnailPath = null;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
Future<void> _generateThumbnailFile() async {
if (_videoPath == null) return;
setState(() {
_isLoading = true;
_error = null;
});
try {
final tempDir = await getTemporaryDirectory();
final path = await VideoThumbnailOhos.thumbnailFile(
video: _videoPath!,
thumbnailPath: tempDir.path,
imageFormat: _format,
maxHeight: _maxHeight,
maxWidth: _maxWidth,
timeMs: _timeMs,
quality: _quality,
);
setState(() {
_thumbnailPath = path;
_thumbnailData = null;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Video Thumbnail Demo'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 视频选择区域
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('视频文件', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
if (_videoPath != null)
Text(_videoPath!, style: const TextStyle(fontSize: 12)),
const SizedBox(height: 8),
ElevatedButton.icon(
icon: const Icon(Icons.video_library),
label: const Text('选择视频'),
onPressed: _pickVideo,
),
],
),
),
),
const SizedBox(height: 16),
// 参数设置区域
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('缩略图参数', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
// 格式选择
Row(
children: [
const Text('格式: '),
DropdownButton<ImageFormat>(
value: _format,
items: const [
DropdownMenuItem(value: ImageFormat.JPEG, child: Text('JPEG')),
DropdownMenuItem(value: ImageFormat.PNG, child: Text('PNG')),
DropdownMenuItem(value: ImageFormat.WEBP, child: Text('WebP')),
],
onChanged: (v) => setState(() => _format = v!),
),
],
),
// 质量滑块
Row(
children: [
const Text('质量: '),
Expanded(
child: Slider(
value: _quality.toDouble(),
min: 0,
max: 100,
divisions: 100,
label: '$_quality',
onChanged: (v) => setState(() => _quality = v.toInt()),
),
),
Text('$_quality'),
],
),
// 时间点滑块
Row(
children: [
const Text('时间: '),
Expanded(
child: Slider(
value: _timeMs.toDouble(),
min: 0,
max: 10000,
divisions: 100,
label: '${_timeMs}ms',
onChanged: (v) => setState(() => _timeMs = v.toInt()),
),
),
Text('$_timeMs ms'),
],
),
// 尺寸设置
Row(
children: [
const Text('尺寸: '),
Expanded(
child: Slider(
value: _maxWidth.toDouble(),
min: 0,
max: 500,
divisions: 50,
label: '$_maxWidth',
onChanged: (v) => setState(() {
_maxWidth = v.toInt();
_maxHeight = v.toInt();
}),
),
),
Text('$_maxWidth x $_maxHeight'),
],
),
],
),
),
),
const SizedBox(height: 16),
// 操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.memory),
label: const Text('生成数据'),
onPressed: _videoPath != null && !_isLoading ? _generateThumbnailData : null,
),
ElevatedButton.icon(
icon: const Icon(Icons.save),
label: const Text('生成文件'),
onPressed: _videoPath != null && !_isLoading ? _generateThumbnailFile : null,
),
],
),
const SizedBox(height: 16),
// 结果显示区域
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('缩略图预览', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
if (_isLoading)
const Center(child: CircularProgressIndicator())
else if (_error != null)
Text('错误: $_error', style: const TextStyle(color: Colors.red))
else if (_thumbnailData != null)
Column(
children: [
Image.memory(_thumbnailData!, fit: BoxFit.cover),
const SizedBox(height: 8),
Text('数据大小: ${_thumbnailData!.length} bytes'),
],
)
else if (_thumbnailPath != null)
Column(
children: [
Image.file(File(_thumbnailPath!), fit: BoxFit.cover),
const SizedBox(height: 8),
Text('文件路径: $_thumbnailPath'),
],
)
else
const Center(
child: Icon(Icons.image, size: 100, color: Colors.grey),
),
],
),
),
),
],
),
),
);
}
}
六、高级用法
6.1 带进度指示的缩略图生成
使用 FutureBuilder 实现带进度指示的缩略图生成:
dart
class ThumbnailGenerator extends StatelessWidget {
final String videoPath;
final int width;
final int height;
final int timeMs;
const ThumbnailGenerator({
Key? key,
required this.videoPath,
this.width = 200,
this.height = 200,
this.timeMs = 0,
}) : super(key: key);
Future<Uint8List?> _generateThumbnail() async {
return await VideoThumbnailOhos.thumbnailData(
video: videoPath,
imageFormat: ImageFormat.JPEG,
maxHeight: height,
maxWidth: width,
timeMs: timeMs,
quality: 75,
);
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Uint8List?>(
future: _generateThumbnail(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData && snapshot.data != null) {
return Image.memory(snapshot.data!, fit: BoxFit.cover);
} else if (snapshot.hasError) {
return const Icon(Icons.error, color: Colors.red);
}
return const Icon(Icons.video_library);
}
return const CircularProgressIndicator();
},
);
}
}
6.2 批量生成缩略图
为视频列表批量生成缩略图:
dart
Future<Map<String, Uint8List>> _generateBatchThumbnails(List<String> videoPaths) async {
final Map<String, Uint8List> results = {};
for (final path in videoPaths) {
try {
final bytes = await VideoThumbnailOhos.thumbnailData(
video: path,
imageFormat: ImageFormat.JPEG,
maxHeight: 120,
maxWidth: 160,
quality: 60,
);
if (bytes != null) {
results[path] = bytes;
}
} catch (e) {
debugPrint('Failed to generate thumbnail for $path: $e');
}
}
return results;
}
6.3 网络视频缩略图
⚠️ 注意:OpenHarmony 平台目前对网络视频生成缩略图存在已知问题,建议先将视频下载到本地再生成缩略图。
dart
Future<Uint8List?> _generateNetworkThumbnail(String url) async {
// 建议先下载视频到本地
// 然后使用本地路径生成缩略图
final bytes = await VideoThumbnailOhos.thumbnailData(
video: url,
headers: {
'User-Agent': 'Mozilla/5.0',
'Authorization': 'Bearer token',
},
imageFormat: ImageFormat.JPEG,
maxHeight: 256,
maxWidth: 256,
quality: 50,
);
return bytes;
}
七、常见问题与解决方案
7.1 缩略图生成失败
问题:生成缩略图时返回 null 或抛出异常。
解决方案:
- 检查视频文件路径是否正确
- 确保已配置
READ_IMAGEVIDEO权限 - 检查视频格式是否支持
- 对于网络视频,建议先下载到本地
7.2 缩略图质量不佳
问题:生成的缩略图模糊或质量差。
解决方案:
- 提高输出尺寸(maxHeight/maxWidth)
- 提高质量参数(quality)
- 使用 PNG 格式(无损压缩)
dart
final bytes = await VideoThumbnailOhos.thumbnailData(
video: videoPath,
imageFormat: ImageFormat.PNG,
maxHeight: 400,
maxWidth: 400,
quality: 100,
);
7.3 内存占用过高
问题:批量生成缩略图时内存占用过高。
解决方案:
- 限制缩略图尺寸
- 及时释放不需要的缩略图数据
- 使用文件方式而非内存方式
dart
// 使用文件方式减少内存占用
final path = await VideoThumbnailOhos.thumbnailFile(
video: videoPath,
thumbnailPath: tempDir,
maxHeight: 120,
maxWidth: 160,
quality: 60,
);
八、最佳实践
8.1 缩略图尺寸建议
| 使用场景 | 建议尺寸 | 建议格式 | 建议质量 |
|---|---|---|---|
| 列表预览 | 120 x 160 | JPEG | 60 |
| 详情页预览 | 200 x 300 | JPEG | 75 |
| 全屏预览 | 400 x 600 | JPEG/PNG | 85 |
| 高清预览 | 原始尺寸 | PNG | 100 |
8.2 性能优化建议
- 对于列表显示,使用较小的缩略图尺寸
- 批量生成时考虑使用队列避免并发过高
- 缓存已生成的缩略图避免重复生成
- 使用文件方式存储缩略图便于后续使用
8.3 错误处理建议
dart
Future<Uint8List?> safeGenerateThumbnail(String videoPath) async {
try {
if (!File(videoPath).existsSync()) {
debugPrint('Video file not found: $videoPath');
return null;
}
final bytes = await VideoThumbnailOhos.thumbnailData(
video: videoPath,
imageFormat: ImageFormat.JPEG,
maxHeight: 256,
maxWidth: 256,
quality: 50,
);
return bytes;
} on PlatformException catch (e) {
debugPrint('Platform error: ${e.message}');
return null;
} catch (e) {
debugPrint('Unknown error: $e');
return null;
}
}
九、总结
本文详细介绍了 Flutter for OpenHarmony 中 video_thumbnail 插件的使用方法,包括:
- ✅ 插件的基本概念和特点
- ✅ OpenHarmony 平台的适配说明
- ✅ 依赖配置和权限设置
- ✅ 基础用法和 API 详解
- ✅ 完整的示例代码
- ✅ 高级用法技巧
- ✅ 常见问题与解决方案
- ✅ 最佳实践建议
通过本文的学习,你应该能够在 Flutter for OpenHarmony 项目中熟练使用 video_thumbnail 插件实现视频缩略图生成功能。
📌 参考资源: