在这里插入图片描述

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


🎯 一、组件概述与应用场景

📋 1.1 video_thumbnail 简介

在 Flutter for OpenHarmony 应用开发中,video_thumbnail 是一个非常实用的插件,用于从视频文件中提取缩略图。它支持本地视频和网络视频,可以自定义缩略图的尺寸、格式、质量和提取时间点。

核心特性:

特性 说明
🌐 跨平台支持 支持 Android、iOS、OpenHarmony
📁 多种来源 支持本地视频文件和网络视频 URL
🖼️ 多种格式 支持 JPEG、PNG、WebP 输出格式
📐 尺寸控制 支持设置最大宽高限制
⏱️ 时间点选择 支持指定提取缩略图的时间点
🎨 质量控制 支持设置输出图片质量
💾 两种输出 支持返回文件路径或内存数据
🔗 HTTP 头 支持自定义 HTTP 请求头

💡 1.2 实际应用场景

视频列表预览:视频应用中显示视频缩略图列表。

视频编辑应用:在时间轴上显示视频帧预览。

媒体库管理:管理和展示视频文件的缩略图。

视频分享预览:分享视频前显示预览图。

视频播放器:在播放器中显示视频封面。

🏗️ 1.3 系统架构设计

┌─────────────────────────────────────────────────────────┐
│                    UI 展示层                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│  │ 视频选择器   │  │ 缩略图预览   │  │ 参数设置面板 │     │
│  └─────────────┘  └─────────────┘  └─────────────┘     │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                    业务逻辑层                            │
│  ┌─────────────────────────────────────────────────┐   │
│  │        VideoThumbnailService 缩略图服务          │   │
│  │  • thumbnailData()  • thumbnailFile()           │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                    平台适配层                            │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│  │ Android     │  │ iOS         │  │ OpenHarmony │     │
│  └─────────────┘  └─────────────┘  └─────────────┘     │
└─────────────────────────────────────────────────────────┘

📦 二、项目配置与依赖安装

🔧 2.1 添加依赖配置

打开项目根目录下的 pubspec.yaml 文件,添加以下配置:

dependencies:
  flutter:
    sdk: flutter

  # video_thumbnail - 视频缩略图插件
  video_thumbnail_ohos:
    git:
      url: https://atomgit.com/openharmony-sig/fluttertpc_video_thumbnail.git
      path: ohos

  # image_picker - 图片选择插件
  image_picker:
    git:
      url: "https://atomgit.com/openharmony-sig/fluttertpc_image_picker.git"

配置说明:

  • 使用 git 方式引用开源鸿蒙适配的仓库
  • 本项目适配 Flutter 3.7.12-ohos-1.0.6,SDK 5.0.0(12)

⚠️ 重要提示:对于 OpenHarmony 平台,直接使用 video_thumbnail_ohos 包即可。

📥 2.2 下载依赖

配置完成后,在项目根目录执行以下命令:

flutter pub get

🔐 2.3 权限配置

ohos/entry/src/main/module.json5:

{
  "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:

{
  "string": [
    {
      "name": "read_media_reason",
      "value": "读取视频文件生成缩略图"
    }
  ]
}

⚠️ 重要提示ohos.permission.READ_IMAGEVIDEO 属于 system_basic 级别权限,需要修改应用权限等级。

📱 2.4 支持的功能

功能 说明 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

🔧 三、核心功能详解

🎯 3.1 生成缩略图数据

thumbnailData() 方法直接返回图片的二进制数据:

import 'dart:typed_data';
import 'package:video_thumbnail_ohos/video_thumbnail_ohos.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;
}

📁 3.2 生成缩略图文件

thumbnailFile() 方法将缩略图保存为文件:

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;
}

🖼️ 3.3 显示缩略图

生成缩略图数据后,可以直接使用 Image.memory() 显示:

Widget buildThumbnail(Uint8List? thumbnailData) {
  if (thumbnailData == null) {
    return const Icon(Icons.video_library, size: 100);
  }
  return Image.memory(thumbnailData, fit: BoxFit.cover);
}

⚙️ 3.4 参数说明

参数 类型 说明
video String 视频文件路径或 URL
imageFormat ImageFormat 输出图片格式,默认 PNG
maxHeight int 最大高度,建议设置具体值
maxWidth int 最大宽度,建议设置具体值
timeMs int 提取时间点(毫秒),默认 0
quality int 图片质量(0-100),PNG 忽略此参数

⚠️ 注意maxHeightmaxWidth 建议设置具体值(如 256),设为 0 可能导致错误。


📝 四、完整示例代码

下面是一个完整的智能视频缩略图系统示例,支持本地视频和网络视频:

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 VideoThumbnailApp());
}

class VideoThumbnailApp extends StatelessWidget {
  const VideoThumbnailApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '智能视频缩略图系统',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const MainPage(),
    );
  }
}

class MainPage extends StatefulWidget {
  const MainPage({super.key});

  
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentIndex = 0;

  final List<Widget> _pages = [
    const LocalVideoPage(),
    const NetworkVideoPage(),
    const SettingsPage(),
  ];

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: _pages[_currentIndex],
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) {
          setState(() => _currentIndex = index);
        },
        destinations: const [
          NavigationDestination(icon: Icon(Icons.folder), label: '本地视频'),
          NavigationDestination(icon: Icon(Icons.link), label: '网络视频'),
          NavigationDestination(icon: Icon(Icons.settings), label: '设置'),
        ],
      ),
    );
  }
}

// ============ 本地视频页面 ============

class LocalVideoPage extends StatefulWidget {
  const LocalVideoPage({super.key});

  
  State<LocalVideoPage> createState() => _LocalVideoPageState();
}

class _LocalVideoPageState extends State<LocalVideoPage> {
  final _picker = ImagePicker();
  
  File? _videoFile;
  Uint8List? _thumbnailData;
  bool _isLoading = false;
  String? _error;

  int _maxSize = 256;
  int _quality = 75;
  int _timeMs = 0;
  ImageFormat _format = ImageFormat.JPEG;

  Future<void> _pickVideo() async {
    final video = await _picker.pickVideo(source: ImageSource.gallery);
    if (video != null) {
      setState(() {
        _videoFile = File(video.path);
        _thumbnailData = null;
        _error = null;
      });
    }
  }

  Future<void> _generateThumbnail() async {
    if (_videoFile == null) return;

    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final bytes = await VideoThumbnailOhos.thumbnailData(
        video: _videoFile!.path,
        imageFormat: _format,
        maxHeight: _maxSize,
        maxWidth: _maxSize,
        timeMs: _timeMs,
        quality: _quality,
      );

      setState(() {
        _thumbnailData = bytes;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('本地视频'),
        centerTitle: true,
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : _buildContent(),
    );
  }

  Widget _buildContent() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(20),
      child: Column(
        children: [
          _buildVideoPreview(),
          const SizedBox(height: 24),
          _buildSettingsCard(),
          const SizedBox(height: 24),
          _buildActionButtons(),
          if (_error != null) ...[
            const SizedBox(height: 16),
            _buildErrorCard(),
          ],
        ],
      ),
    );
  }

  Widget _buildVideoPreview() {
    return Container(
      height: 250,
      decoration: BoxDecoration(
        color: Colors.grey.shade200,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Center(
        child: _thumbnailData != null
            ? ClipRRect(
                borderRadius: BorderRadius.circular(16),
                child: Image.memory(_thumbnailData!, fit: BoxFit.cover),
              )
            : _videoFile != null
                ? Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const Icon(Icons.video_file, size: 64, color: Colors.blue),
                      const SizedBox(height: 8),
                      Text('视频已选择', style: TextStyle(color: Colors.grey.shade600)),
                    ],
                  )
                : Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(Icons.video_library, size: 64, color: Colors.grey.shade400),
                      const SizedBox(height: 8),
                      Text('点击下方按钮选择视频', style: TextStyle(color: Colors.grey.shade500)),
                    ],
                  ),
      ),
    );
  }

  Widget _buildSettingsCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('缩略图设置', style: TextStyle(fontWeight: FontWeight.bold)),
            const SizedBox(height: 16),
            Row(
              children: [
                const Text('格式: '),
                const SizedBox(width: 8),
                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!),
                ),
              ],
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                const Text('尺寸: '),
                Expanded(
                  child: Slider(
                    value: _maxSize.toDouble(),
                    min: 64,
                    max: 512,
                    divisions: 8,
                    label: '$_maxSize',
                    onChanged: (v) => setState(() => _maxSize = v.toInt()),
                  ),
                ),
                Text('$_maxSize'),
              ],
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                const Text('质量: '),
                Expanded(
                  child: Slider(
                    value: _quality.toDouble(),
                    min: 0,
                    max: 100,
                    divisions: 10,
                    label: '$_quality%',
                    onChanged: (v) => setState(() => _quality = v.toInt()),
                  ),
                ),
                Text('$_quality%'),
              ],
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                const Text('时间点: '),
                Expanded(
                  child: Slider(
                    value: _timeMs.toDouble(),
                    min: 0,
                    max: 10000,
                    divisions: 20,
                    label: '${_timeMs}ms',
                    onChanged: (v) => setState(() => _timeMs = v.toInt()),
                  ),
                ),
                Text('$_timeMs ms'),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildActionButtons() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        ElevatedButton.icon(
          icon: const Icon(Icons.video_library),
          label: const Text('选择视频'),
          onPressed: _pickVideo,
        ),
        ElevatedButton.icon(
          icon: const Icon(Icons.image),
          label: const Text('生成缩略图'),
          onPressed: _videoFile != null ? _generateThumbnail : null,
          style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
        ),
      ],
    );
  }

  Widget _buildErrorCard() {
    return Card(
      color: Colors.red.shade50,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            const Icon(Icons.error, color: Colors.red),
            const SizedBox(width: 12),
            Expanded(
              child: Text(_error!, style: const TextStyle(color: Colors.red)),
            ),
          ],
        ),
      ),
    );
  }
}

// ============ 网络视频页面 ============

class NetworkVideoPage extends StatefulWidget {
  const NetworkVideoPage({super.key});

  
  State<NetworkVideoPage> createState() => _NetworkVideoPageState();
}

class _NetworkVideoPageState extends State<NetworkVideoPage> {
  final _urlController = TextEditingController();
  
  Uint8List? _thumbnailData;
  bool _isLoading = false;
  String? _error;

  int _maxSize = 256;
  int _quality = 75;
  int _timeMs = 0;
  ImageFormat _format = ImageFormat.JPEG;

  final List<String> _presetUrls = [
    'https://www.w3schools.com/html/mov_bbb.mp4',
    'https://sample-videos.com/video321/mp4/720/big_buck_bunny_720p_1mb.mp4',
  ];

  Future<void> _generateThumbnail() async {
    final url = _urlController.text.trim();
    if (url.isEmpty) return;

    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final bytes = await VideoThumbnailOhos.thumbnailData(
        video: url,
        imageFormat: _format,
        maxHeight: _maxSize,
        maxWidth: _maxSize,
        timeMs: _timeMs,
        quality: _quality,
      );

      setState(() {
        _thumbnailData = bytes;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  void _usePresetUrl(String url) {
    _urlController.text = url;
    setState(() {
      _thumbnailData = null;
      _error = null;
    });
  }

  
  void dispose() {
    _urlController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('网络视频'),
        centerTitle: true,
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : _buildContent(),
    );
  }

  Widget _buildContent() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(20),
      child: Column(
        children: [
          _buildUrlInput(),
          const SizedBox(height: 16),
          _buildPresetUrls(),
          const SizedBox(height: 24),
          _buildThumbnailPreview(),
          const SizedBox(height: 24),
          _buildSettingsCard(),
          const SizedBox(height: 24),
          _buildGenerateButton(),
          if (_error != null) ...[
            const SizedBox(height: 16),
            _buildErrorCard(),
          ],
        ],
      ),
    );
  }

  Widget _buildUrlInput() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('视频URL', style: TextStyle(fontWeight: FontWeight.bold)),
            const SizedBox(height: 12),
            TextField(
              controller: _urlController,
              decoration: InputDecoration(
                hintText: '输入网络视频URL',
                prefixIcon: const Icon(Icons.link),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
                suffixIcon: IconButton(
                  icon: const Icon(Icons.clear),
                  onPressed: () {
                    _urlController.clear();
                    setState(() {
                      _thumbnailData = null;
                      _error = null;
                    });
                  },
                ),
              ),
              onSubmitted: (_) => _generateThumbnail(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildPresetUrls() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('示例视频', style: TextStyle(fontWeight: FontWeight.bold)),
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: _presetUrls.map((url) {
                return ActionChip(
                  label: Text(
                    url.split('/').last,
                    style: const TextStyle(fontSize: 12),
                  ),
                  avatar: const Icon(Icons.play_circle, size: 18),
                  onPressed: () => _usePresetUrl(url),
                );
              }).toList(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildThumbnailPreview() {
    return Container(
      height: 200,
      decoration: BoxDecoration(
        color: Colors.grey.shade200,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Center(
        child: _thumbnailData != null
            ? ClipRRect(
                borderRadius: BorderRadius.circular(16),
                child: Image.memory(_thumbnailData!, fit: BoxFit.cover),
              )
            : Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.video_library, size: 64, color: Colors.grey.shade400),
                  const SizedBox(height: 8),
                  Text('输入URL并生成缩略图', style: TextStyle(color: Colors.grey.shade500)),
                ],
              ),
      ),
    );
  }

  Widget _buildSettingsCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('缩略图设置', style: TextStyle(fontWeight: FontWeight.bold)),
            const SizedBox(height: 16),
            Row(
              children: [
                const Text('格式: '),
                const SizedBox(width: 8),
                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!),
                ),
              ],
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                const Text('尺寸: '),
                Expanded(
                  child: Slider(
                    value: _maxSize.toDouble(),
                    min: 64,
                    max: 512,
                    divisions: 8,
                    label: '$_maxSize',
                    onChanged: (v) => setState(() => _maxSize = v.toInt()),
                  ),
                ),
                Text('$_maxSize'),
              ],
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                const Text('质量: '),
                Expanded(
                  child: Slider(
                    value: _quality.toDouble(),
                    min: 0,
                    max: 100,
                    divisions: 10,
                    label: '$_quality%',
                    onChanged: (v) => setState(() => _quality = v.toInt()),
                  ),
                ),
                Text('$_quality%'),
              ],
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                const Text('时间点: '),
                Expanded(
                  child: Slider(
                    value: _timeMs.toDouble(),
                    min: 0,
                    max: 10000,
                    divisions: 20,
                    label: '${_timeMs}ms',
                    onChanged: (v) => setState(() => _timeMs = v.toInt()),
                  ),
                ),
                Text('$_timeMs ms'),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildGenerateButton() {
    return SizedBox(
      width: double.infinity,
      child: ElevatedButton.icon(
        icon: const Icon(Icons.image),
        label: const Text('生成缩略图'),
        onPressed: _urlController.text.isNotEmpty ? _generateThumbnail : null,
        style: ElevatedButton.styleFrom(
          backgroundColor: Colors.blue,
          foregroundColor: Colors.white,
          padding: const EdgeInsets.symmetric(vertical: 16),
        ),
      ),
    );
  }

  Widget _buildErrorCard() {
    return Card(
      color: Colors.red.shade50,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            const Icon(Icons.error, color: Colors.red),
            const SizedBox(width: 12),
            Expanded(
              child: Text(_error!, style: const TextStyle(color: Colors.red)),
            ),
          ],
        ),
      ),
    );
  }
}

// ============ 设置页面 ============

class SettingsPage extends StatelessWidget {
  const SettingsPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('设置'),
        centerTitle: true,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            _buildInfoCard(),
            const SizedBox(height: 24),
            _buildSupportedFormatsCard(),
            const SizedBox(height: 24),
            _buildTipsCard(),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoCard() {
    return Container(
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [Colors.blue.shade400, Colors.blue.shade600],
        ),
        borderRadius: BorderRadius.circular(16),
      ),
      child: const Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Icon(Icons.info_outline, color: Colors.white),
              SizedBox(width: 8),
              Text(
                '关于视频缩略图',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ],
          ),
          SizedBox(height: 12),
          Text(
            'video_thumbnail 插件可以从视频中提取缩略图,支持本地视频和网络视频(需要 SDK API >= 20)。',
            style: TextStyle(color: Colors.white70, height: 1.5),
          ),
        ],
      ),
    );
  }

  Widget _buildSupportedFormatsCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('支持的输出格式', style: TextStyle(fontWeight: FontWeight.bold)),
            const SizedBox(height: 16),
            _buildFormatItem('JPEG', '有损压缩,文件小'),
            _buildFormatItem('PNG', '无损压缩,质量高'),
            _buildFormatItem('WebP', '现代格式,压缩率高'),
          ],
        ),
      ),
    );
  }

  Widget _buildFormatItem(String format, String description) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Container(
            width: 8,
            height: 8,
            decoration: BoxDecoration(
              color: Colors.blue.shade400,
              shape: BoxShape.circle,
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(format, style: const TextStyle(fontWeight: FontWeight.w500)),
                Text(description, style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTipsCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('使用提示', style: TextStyle(fontWeight: FontWeight.bold)),
            const SizedBox(height: 16),
            _buildTipItem('网络视频需要 SDK API >= 20'),
            _buildTipItem('建议设置具体的 maxHeight/maxWidth 值'),
            _buildTipItem('时间点参数单位为毫秒'),
            _buildTipItem('PNG 格式会忽略质量参数'),
          ],
        ),
      ),
    );
  }

  Widget _buildTipItem(String text) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        children: [
          Icon(Icons.check_circle, color: Colors.green.shade400, size: 20),
          const SizedBox(width: 12),
          Expanded(child: Text(text)),
        ],
      ),
    );
  }
}

🏆 五、最佳实践与注意事项

⚠️ 5.1 权限配置

system_basic 权限READ_IMAGEVIDEO 属于 system_basic 级别权限,需要修改应用权限等级。

签名配置:修改签名模板文件以支持高级权限。

🔍 5.2 性能优化

设置合理尺寸:不要设置过大的 maxHeight/maxWidth,避免内存问题。

选择合适格式:JPEG 适合照片类,PNG 适合需要透明背景的场景。

批量处理:对于大量视频,建议分批处理避免内存溢出。

📱 5.3 常见问题处理

FetchFrameByTime failed:确保 maxHeight/maxWidth 设置具体值而非 0。

网络视频不支持:检查 SDK API 版本是否 >= 20。

权限错误 9568289:需要修改应用权限等级为 system_basic。


📌 六、总结

本文通过一个完整的智能视频缩略图系统案例,深入讲解了 video_thumbnail 插件的使用方法与最佳实践:

基础生成:掌握从视频提取缩略图的基本方法。

参数配置:了解各种参数的作用和最佳设置。

批量处理:实现批量视频缩略图生成功能。

权限管理:正确配置 system_basic 级别权限。

掌握这些技巧,你就能构建出专业级的视频缩略图功能,满足各种业务场景需求。


参考资料

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐