Flutter视频编辑插件video_editor_pits的使用

Flutter视频编辑插件video_editor_pits的使用

Flutter视频编辑插件

Pub GitHub stars

video_editor_pits 是一个视频编辑库,允许用户对视频进行裁剪、旋转、缩放等操作,并可以选择封面。该库提供了执行导出的一些工具,但不处理导出。

该库仅用 Dart 编写,但使用了外部包如 video_thumbnail,使其目前仅支持 iOS 和 Android 平台(web 支持正在进行中)。

注意

如果你使用的是 1.2.3 到 2.4.0 之间的版本,你的项目可能受到 GPL 许可证的影响。

Android iOS
支持 SDK 16+ 11.0+

安装

以下是将此库添加到 Flutter 项目的步骤:

  1. 在终端运行 flutter pub add video_editor,或手动在 pubspec.yaml 文件中添加依赖项。
dependencies:
  video_editor: ^2.4.0
  1. 在代码中导入该包:
import 'package:video_editor_pits/video_editor.dart';

截图

示例应用运行在 iPhone 11 Pro 上 自定义示例,浅色模式

使用

以下是一个完整的示例代码,展示了如何使用 video_editor_pits 插件来编辑视频:

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:video_editor/video_editor.dart';

void main() => runApp(
      MaterialApp(
        title: 'Flutter Video Editor Demo',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.grey,
          brightness: Brightness.dark,
          tabBarTheme: const TabBarTheme(
            indicator: UnderlineTabIndicator(
              borderSide: BorderSide(color: Colors.white),
            ),
          ),
          dividerColor: Colors.white,
        ),
        home: const VideoEditorExample(),
      ),
    );

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

  [@override](/user/override)
  State<VideoEditorExample> createState() => _VideoEditorExampleState();
}

class _VideoEditorExampleState extends State<VideoEditorExample> {
  final ImagePicker _picker = ImagePicker();

  void _pickVideo() async {
    final XFile? file = await _picker.pickVideo(source: ImageSource.gallery);

    if (mounted && file != null) {
      Navigator.push(
        context,
        MaterialPageRoute<void>(
          builder: (BuildContext context) => VideoEditor(file: File(file.path)),
        ),
      );
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Video Picker")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text("点击按钮选择视频"),
            ElevatedButton(
              onPressed: _pickVideo,
              child: const Text("从相册选择视频"),
            ),
          ],
        ),
      ),
    );
  }
}

//-------------------//
//VIDEO EDITOR SCREEN//
//-------------------//
class VideoEditor extends StatefulWidget {
  const VideoEditor({super.key, required this.file});

  final File file;

  [@override](/user/override)
  State<VideoEditor> createState() => _VideoEditorState();
}

class _VideoEditorState extends State<VideoEditor> {
  final _exportingProgress = ValueNotifier<double>(0.0);
  final _isExporting = ValueNotifier<bool>(false);
  final double height = 60;

  late final VideoEditorController _controller = VideoEditorController.file(
    widget.file,
    minDuration: const Duration(seconds: 1),
    maxDuration: const Duration(seconds: 10),
  );

  [@override](/user/override)
  void initState() {
    super.initState();
    _controller
        .initialize(aspectRatio: 9 / 16)
        .then((_) => setState(() {}))
        .catchError((error) {
      // 处理最小持续时间大于视频持续时间错误
      Navigator.pop(context);
    }, test: (e) => e is VideoMinDurationError);
  }

  [@override](/user/override)
  void dispose() async {
    _exportingProgress.dispose();
    _isExporting.dispose();
    _controller.dispose();
    super.dispose();
  }

  void _showErrorSnackBar(String message) =>
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(message),
          duration: const Duration(seconds: 1),
        ),
      );

  void _exportVideo() async {
    _exportingProgress.value = 0;
    _isExporting.value = true;

    final config = VideoFFmpegVideoEditorConfig(
      _controller,
      // format: VideoExportFormat.gif,
      // commandBuilder: (config, videoPath, outputPath) {
      //   final List<String> filters = config.getExportFilters();
      //   filters.add('hflip'); // 添加水平翻转

      //   return '-i $videoPath ${config.filtersCmd(filters)} -preset ultrafast $outputPath';
      // },
    );

    await ExportService.runFFmpegCommand(
      await config.getExecuteConfig(),
      onProgress: (stats) {
        _exportingProgress.value = config.getFFmpegProgress(stats.getTime());
      },
      onError: (e, s) => _showErrorSnackBar("导出视频时发生错误 :("),
      onCompleted: (file) {
        _isExporting.value = false;
        if (!mounted) return;

        showDialog(
          context: context,
          builder: (_) => VideoResultPopup(video: file),
        );
      },
    );
  }

  void _exportCover() async {
    final config = CoverFFmpegVideoEditorConfig(_controller);
    final execute = await config.getExecuteConfig();
    if (execute == null) {
      _showErrorSnackBar("导出封面初始化错误。");
      return;
    }

    await ExportService.runFFmpegCommand(
      execute,
      onError: (e, s) => _showErrorSnackBar("导出封面时发生错误 :("),
      onCompleted: (cover) {
        if (!mounted) return;

        showDialog(
          context: context,
          builder: (_) => CoverResultPopup(cover: cover),
        );
      },
    );
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async => false,
      child: Scaffold(
        backgroundColor: Colors.black,
        body: _controller.initialized
            ? SafeArea(
                child: Stack(
                  children: [
                    Column(
                      children: [
                        _topNavBar(),
                        Expanded(
                          child: DefaultTabController(
                            length: 2,
                            child: Column(
                              children: [
                                Expanded(
                                  child: TabBarView(
                                    physics: const NeverScrollableScrollPhysics(),
                                    children: [
                                      Stack(
                                        alignment: Alignment.center,
                                        children: [
                                          CropGridViewer.preview(
                                              controller: _controller),
                                          AnimatedBuilder(
                                            animation: _controller.video,
                                            builder: (_, __) => AnimatedOpacity(
                                              opacity:
                                                  _controller.isPlaying ? 0 : 1,
                                              duration: kThemeAnimationDuration,
                                              child: GestureDetector(
                                                onTap: _controller.video.play,
                                                child: Container(
                                                  width: 40,
                                                  height: 40,
                                                  decoration:
                                                      const BoxDecoration(
                                                    color: Colors.white,
                                                    shape: BoxShape.circle,
                                                  ),
                                                  child: const Icon(
                                                    Icons.play_arrow,
                                                    color: Colors.black,
                                                  ),
                                                ),
                                              ),
                                            ),
                                          ),
                                        ],
                                      ),
                                      CoverViewer(controller: _controller)
                                    ],
                                  ),
                                ),
                                Container(
                                  height: 200,
                                  margin: const EdgeInsets.only(top: 10),
                                  child: Column(
                                    children: [
                                      const TabBar(
                                        tabs: [
                                          Row(
                                              mainAxisAlignment:
                                                  MainAxisAlignment.center,
                                              children: [
                                                Padding(
                                                    padding: EdgeInsets.all(5),
                                                    child: Icon(Icons.content_cut)),
                                                Text('Trim')
                                              ]),
                                          Row(
                                            mainAxisAlignment:
                                                MainAxisAlignment.center,
                                            children: [
                                              Padding(
                                                  padding: EdgeInsets.all(5),
                                                  child: Icon(Icons.video_label)),
                                              Text('Cover')
                                            ],
                                          ),
                                        ],
                                      ),
                                      Expanded(
                                        child: TabBarView(
                                          physics:
                                              const NeverScrollableScrollPhysics(),
                                          children: [
                                            Column(
                                              mainAxisAlignment:
                                                  MainAxisAlignment.center,
                                              children: _trimSlider(),
                                            ),
                                            _coverSelection(),
                                          ],
                                        ),
                                      ),
                                    ],
                                  ),
                                ),
                                ValueListenableBuilder(
                                  valueListenable: _isExporting,
                                  builder: (_, bool export, Widget? child) =>
                                      AnimatedSize(
                                    duration: kThemeAnimationDuration,
                                    child: export ? child : null,
                                  ),
                                  child: AlertDialog(
                                    title: ValueListenableBuilder(
                                      valueListenable: _exportingProgress,
                                      builder: (_, double value, __) => Text(
                                        "导出视频 ${(value * 100).ceil()}%",
                                        style: const TextStyle(fontSize: 12),
                                      ),
                                    ),
                                  ),
                                )
                              ],
                            ),
                          ),
                        )
                      ],
                    )
                  ],
                ),
              )
            : const Center(child: CircularProgressIndicator()),
      ),
    );
  }

  Widget _topNavBar() {
    return SafeArea(
      child: SizedBox(
        height: height,
        child: Row(
          children: [
            Expanded(
              child: IconButton(
                onPressed: () => Navigator.of(context).pop(),
                icon: const Icon(Icons.exit_to_app),
                tooltip: '离开编辑器',
              ),
            ),
            const VerticalDivider(endIndent: 22, indent: 22),
            Expanded(
              child: IconButton(
                onPressed: () =>
                    _controller.rotate90Degrees(RotateDirection.left),
                icon: const Icon(Icons.rotate_left),
                tooltip: '逆时针旋转',
              ),
            ),
            Expanded(
              child: IconButton(
                onPressed: () =>
                    _controller.rotate90Degrees(RotateDirection.right),
                icon: const Icon(Icons.rotate_right),
                tooltip: '顺时针旋转',
              ),
            ),
            Expanded(
              child: IconButton(
                onPressed: () => Navigator.push(
                  context,
                  MaterialPageRoute<void>(
                    builder: (context) => CropPage(controller: _controller),
                  ),
                ),
                icon: const Icon(Icons.crop),
                tooltip: '打开裁剪屏幕',
              ),
            ),
            const VerticalDivider(endIndent: 22, indent: 22),
            Expanded(
              child: PopupMenuButton(
                tooltip: '打开导出菜单',
                icon: const Icon(Icons.save),
                itemBuilder: (context) => [
                  PopupMenuItem(
                    onTap: _exportCover,
                    child: const Text('导出封面'),
                  ),
                  PopupMenuItem(
                    onTap: _exportVideo,
                    child: const Text('导出视频'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  String formatter(Duration duration) =>
      [
        duration.inMinutes.remainder(60).toString().padLeft(2, '0'),
        duration.inSeconds.remainder(60).toString().padLeft(2, '0')
      ].join(":");

  List<Widget> _trimSlider() {
    return [
      AnimatedBuilder(
        animation: Listenable.merge([
          _controller,
          _controller.video,
        ]),
        builder: (_, __) {
          final int duration = _controller.videoDuration.inSeconds;
          final double pos = _controller.trimPosition * duration;

          return Padding(
            padding: EdgeInsets.symmetric(horizontal: height / 4),
            child: Row(children: [
              Text(formatter(Duration(seconds: pos.toInt()))),
              const Expanded(child: SizedBox()),
              AnimatedOpacity(
                opacity: _controller.isTrimming ? 1 : 0,
                duration: kThemeAnimationDuration,
                child: Row(mainAxisSize: MainAxisSize.min, children: [
                  Text(formatter(_controller.startTrim)),
                  const SizedBox(width: 10),
                  Text(formatter(_controller.endTrim)),
                ]),
              ),
            ]),
          );
        },
      ),
      Container(
        width: MediaQuery.of(context).size.width,
        margin: EdgeInsets.symmetric(vertical: height / 4),
        child: TrimSlider(
          controller: _controller,
          height: height,
          horizontalMargin: height / 4,
          child: TrimTimeline(
            controller: _controller,
            padding: const EdgeInsets.only(top: 10),
          ),
        ),
      )
    ];
  }

  Widget _coverSelection() {
    return SingleChildScrollView(
      child: Center(
        child: Container(
          margin: const EdgeInsets.all(15),
          child: CoverSelection(
            controller: _controller,
            size: height + 10,
            quantity: 8,
            selectedCoverBuilder: (cover, size) {
              return Stack(
                alignment: Alignment.center,
                children: [
                  cover,
                  Icon(
                    Icons.check_circle,
                    color: const CoverSelectionStyle().selectedBorderColor,
                  )
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

更多关于Flutter视频编辑插件video_editor_pits的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter视频编辑插件video_editor_pits的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


video_editor_pits 是一个用于 Flutter 的视频编辑插件,它可以帮助你在 Flutter 应用中实现视频裁剪、压缩、添加水印等功能。以下是如何使用 video_editor_pits 插件的步骤:

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 video_editor_pits 插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  video_editor_pits: ^1.0.0  # 请使用最新版本

然后运行 flutter pub get 来获取依赖。

2. 导入插件

在你的 Dart 文件中导入 video_editor_pits 插件:

import 'package:video_editor_pits/video_editor_pits.dart';

3. 初始化插件

在使用插件之前,你需要先初始化它:

VideoEditorPits videoEditor = VideoEditorPits();

4. 裁剪视频

你可以使用 cropVideo 方法来裁剪视频。以下是一个示例:

String inputPath = '/path/to/input/video.mp4';
String outputPath = '/path/to/output/video.mp4';
int startTime = 1000; // 开始时间(毫秒)
int endTime = 5000;   // 结束时间(毫秒)

videoEditor.cropVideo(
  inputPath: inputPath,
  outputPath: outputPath,
  startTime: startTime,
  endTime: endTime,
).then((result) {
  if (result) {
    print('视频裁剪成功');
  } else {
    print('视频裁剪失败');
  }
});

5. 压缩视频

你可以使用 compressVideo 方法来压缩视频。以下是一个示例:

String inputPath = '/path/to/input/video.mp4';
String outputPath = '/path/to/output/video.mp4';
int quality = 50; // 质量百分比(0-100)

videoEditor.compressVideo(
  inputPath: inputPath,
  outputPath: outputPath,
  quality: quality,
).then((result) {
  if (result) {
    print('视频压缩成功');
  } else {
    print('视频压缩失败');
  }
});

6. 添加水印

你可以使用 addWatermark 方法来为视频添加水印。以下是一个示例:

String inputPath = '/path/to/input/video.mp4';
String outputPath = '/path/to/output/video.mp4';
String watermarkPath = '/path/to/watermark.png'; // 水印图片路径
WatermarkPosition position = WatermarkPosition.bottomRight; // 水印位置

videoEditor.addWatermark(
  inputPath: inputPath,
  outputPath: outputPath,
  watermarkPath: watermarkPath,
  position: position,
).then((result) {
  if (result) {
    print('水印添加成功');
  } else {
    print('水印添加失败');
  }
});

7. 处理视频

你可以使用 processVideo 方法来同时进行裁剪、压缩和添加水印。以下是一个示例:

String inputPath = '/path/to/input/video.mp4';
String outputPath = '/path/to/output/video.mp4';
int startTime = 1000; // 开始时间(毫秒)
int endTime = 5000;   // 结束时间(毫秒)
int quality = 50;     // 质量百分比(0-100)
String watermarkPath = '/path/to/watermark.png'; // 水印图片路径
WatermarkPosition position = WatermarkPosition.bottomRight; // 水印位置

videoEditor.processVideo(
  inputPath: inputPath,
  outputPath: outputPath,
  startTime: startTime,
  endTime: endTime,
  quality: quality,
  watermarkPath: watermarkPath,
  position: position,
).then((result) {
  if (result) {
    print('视频处理成功');
  } else {
    print('视频处理失败');
  }
});

8. 处理异常

在使用插件时,可能会遇到各种异常。你可以使用 try-catch 来捕获并处理这些异常:

try {
  await videoEditor.cropVideo(
    inputPath: inputPath,
    outputPath: outputPath,
    startTime: startTime,
    endTime: endTime,
  );
} catch (e) {
  print('发生异常: $e');
}
回到顶部