在这里插入图片描述

欢迎加入开源鸿蒙跨平台社区: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 快速生成视频缩略图
状态管理:清晰的状态管理和用户反馈机制
错误处理:完善的错误处理和用户提示

这个应用为用户提供了完整的视频内容创作工具,从视频选择、预览、缩略图生成到分享,一气呵成!


Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐