Flutter视频字幕编辑插件video_subtitle_editor的使用

Flutter视频字幕编辑插件video_subtitle_editor的使用

简介

video_subtitle_editor 是一个用于 Flutter 的视频字幕编辑库,支持字幕生成、字幕编辑、字幕导出以及相关的用户界面组件。

功能 Android 支持 iOS 支持
支持 SDK 16+ 11.0+

安装

在您的 Flutter 项目中添加此库作为依赖项,可以按照以下步骤操作:

  1. 在终端运行命令:

    flutter pub add video_subtitle_editor

    或者手动将以下内容添加到 pubspec.yaml 文件中:

    dependencies:
      video_subtitle_editor: ^1.0.0
  2. 在代码中导入包:

    import 'package:video_subtitle_editor/video_subtitle_editor.dart';

截图

字幕滑块 编辑文本
字幕滑块 编辑文本

使用方法

1. 初始化字幕控制器

可以通过文件或资源初始化字幕控制器。

late final VideoSubtitleController _controller = VideoSubtitleController.file(
  widget.videoFile,
);

[@override](/user/override)
void initState() {
  super.initState();
  var subtitlePath = "assets/test.srt";
  var controller = SubtitleController(
    provider: AssetSubtitle(subtitlePath),
  );
  _controller
      .initializeVideo()
      .then((_) => setState(() {}))
      .catchError((error) {});
  _controller.initialSubtitles(controller);
  _controller.addListener(() {
    setState(() {});
  });
}

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

2. 添加视频查看器到你的 Widget 树中

VideoViewer(
  controller: controller,
  child: SubtitleTextView(
    controller: controller,
  ),
);

3. 添加字幕查看器

SubtitleSlider(
  height: 100,
  controller: _controller,
),

示例代码

以下是一个完整的示例代码,展示如何使用 video_subtitle_editor 插件进行视频字幕编辑。

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_subtitle_editor/video_subtitle_editor.dart';
import 'widgets/export_result.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(isUseDemo) async {
    if (!isUseDemo) {
      final XFile? file = await _picker.pickVideo(source: ImageSource.gallery);

      if (mounted && file != null) {
        Navigator.push(
          context,
          MaterialPageRoute<void>(
            builder: (BuildContext context) =>
                VideoEditor(sourceType: DataSourceType.file, filePath: file.path),
          ),
        );
      }
    } else {
      Navigator.push(
        context,
        MaterialPageRoute<void>(
          builder: (BuildContext context) =>
              VideoEditor(sourceType: DataSourceType.asset, filePath: "assets/test.mp4"),
        ),
      );
    }
  }

  [@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("Click on the button to select video"),
            ElevatedButton(
              onPressed: () {
                _pickVideo(false);
              },
              child: const Text("Pick Video From Gallery"),
            ),
            ElevatedButton(
              onPressed: () {
                _pickVideo(true);
              },
              child: const Text("Add demo Video"),
            ),
          ],
        ),
      ),
    );
  }
}

//-------------------//
// VIDEO EDITOR SCREEN //
//-------------------//

class VideoEditor extends StatefulWidget {
  const VideoEditor({super.key, required this.sourceType, required this.filePath});

  final String filePath;
  final DataSourceType sourceType;

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

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

  [@override](/user/override)
  void initState() {
    super.initState();
    if (widget.sourceType == DataSourceType.file) {
      _controller = VideoSubtitleController.file(widget.filePath);
    } else if (widget.sourceType == DataSourceType.asset) {
      _controller = VideoSubtitleController.asset(widget.filePath);
    }
    var subtitlePath = "assets/test.srt";
    var controller = SubtitleController(
      provider: AssetSubtitle(subtitlePath),
    );
    _controller
        .initializeVideo()
        .then((_) => setState(() {}))
        .catchError((error) {});
    _controller.initialSubtitles(controller);
    _controller.addListener(() {
      setState(() {});
    });
  }

  [@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: 10),
        ),
      );

  double getFFmpegProgress(double time) {
    final double progressValue =
        time / _controller.videoPosition.inMilliseconds;
    return progressValue.clamp(0.0, 1.0);
  }

  void _exportVideo() async {
    _exportingProgress.value = 0;
    _isExporting.value = true;
    // 如何基于 _controller.subtitles 生成字幕文件
    String content = _controller.generateSubtitleContent();
    var subtitlePath = await ExportService.createTempSubtitleFile(content);
    var videoOutputPath = await ExportService.generateOutputPath();
    await ExportService.exportVideoWithSubtitles(
      videoPath: widget.filePath,
      subtitlePath: subtitlePath,
      outputPath: videoOutputPath,
      onProgress: (stats) {
        _exportingProgress.value = getFFmpegProgress(stats.getTime());
      },
      onError: (e, s) {
        _isExporting.value = false;
        if (!mounted) return;
        print("export Error on export video :( $s");
        _showErrorSnackBar("Error on export video :( $e");
      },
      onCompleted: (file) {
        _isExporting.value = false;
        if (!mounted) return;
        showDialog(
          context: context,
          builder: (_) => VideoResultPopup(video: file),
        );
      },
    );
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        _controller.dismissHighlightedSubtitle();
      },
      child: Scaffold(
        backgroundColor: Colors.black,
        body: _controller.initialized
            ? SafeArea(
                child: Stack(
                  children: [
                    Column(
                      children: [
                        _topNavBar(),
                        Expanded(
                          child: buildVideoView(_controller),
                        ),
                        Text(
                          "${formatter(_controller.videoPosition)}/${formatter(_controller.videoDuration)}",
                          style: const TextStyle(
                              color: Colors.white, fontSize: 16),
                        ),
                        Container(
                          margin: const EdgeInsets.only(top: 10, bottom: 50),
                          child: SubtitleSlider(
                            height: 100,
                            controller: _controller,
                          ),
                        ),
                        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(
                                "Exporting video ${(value * 100).ceil()}%",
                                style: const TextStyle(fontSize: 12),
                              ),
                            ),
                          ),
                        )
                      ],
                    ),
                  ],
                ),
              )
            : const Center(child: CircularProgressIndicator()),
      ),
    );
  }

  Widget _topNavBar() {
    return SafeArea(
      child: SizedBox(
        height: 100,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Expanded(
              child: IconButton(
                onPressed: () => Navigator.of(context).pop(),
                icon: const Icon(Icons.exit_to_app),
                tooltip: 'Leave editor',
              ),
            ),
            const VerticalDivider(endIndent: 22, indent: 22),
            Expanded(
              child: PopupMenuButton(
                tooltip: 'Open export menu',
                icon: const Icon(Icons.save),
                itemBuilder: (context) => [
                  PopupMenuItem(
                    onTap: _exportVideo,
                    child: const Text('Export video'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  /// 返回带有编辑视图的 [VideoViewer]
  /// 在视频区域外绘制矩形框
  Widget buildVideoView(VideoSubtitleController controller) {
    return VideoViewer(
      controller: controller,
      child: SubtitleTextView(
        controller: controller,
      ),
    );
  }

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

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


video_subtitle_editor 是一个用于在 Flutter 应用中编辑视频字幕的插件。它允许用户在视频上添加、编辑和同步字幕。以下是如何使用 video_subtitle_editor 插件的基本步骤:

1. 添加依赖

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

dependencies:
  flutter:
    sdk: flutter
  video_subtitle_editor: ^latest_version

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

2. 导入插件

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

import 'package:video_subtitle_editor/video_subtitle_editor.dart';

3. 使用 VideoSubtitleEditor

你可以使用 VideoSubtitleEditor 小部件来加载视频并编辑字幕。以下是一个简单的示例:

class VideoEditorScreen extends StatefulWidget {
  @override
  _VideoEditorScreenState createState() => _VideoEditorScreenState();
}

class _VideoEditorScreenState extends State<VideoEditorScreen> {
  final VideoSubtitleEditorController _controller = VideoSubtitleEditorController();

  @override
  void initState() {
    super.initState();
    _controller.loadVideo('path_to_your_video.mp4');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Video Subtitle Editor'),
      ),
      body: Column(
        children: [
          Expanded(
            child: VideoSubtitleEditor(
              controller: _controller,
            ),
          ),
          ElevatedButton(
            onPressed: () {
              // 添加字幕
              _controller.addSubtitle(
                Subtitle(
                  start: Duration(seconds: 0),
                  end: Duration(seconds: 5),
                  text: 'Hello, World!',
                ),
              );
            },
            child: Text('Add Subtitle'),
          ),
          ElevatedButton(
            onPressed: () async {
              // 保存字幕
              final subtitleFile = await _controller.saveSubtitles();
              print('Subtitles saved to: ${subtitleFile.path}');
            },
            child: Text('Save Subtitles'),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

4. 加载视频

使用 _controller.loadVideo('path_to_your_video.mp4') 来加载视频文件。你可以从本地文件系统或网络加载视频。

5. 添加字幕

使用 _controller.addSubtitle(Subtitle(...)) 来添加字幕。Subtitle 类包含字幕的开始时间、结束时间和文本内容。

6. 保存字幕

使用 _controller.saveSubtitles() 来保存字幕。该方法返回一个 File 对象,你可以将其保存到本地文件系统或上传到服务器。

7. 其他功能

video_subtitle_editor 插件还提供了其他功能,如删除字幕、编辑字幕、调整字幕时间等。你可以通过 VideoSubtitleEditorController 来访问这些功能。

8. 处理字幕文件

你可以使用 SubtitleFile 类来处理字幕文件。例如,你可以加载 .srt.vtt 文件,并将其转换为 Subtitle 对象列表。

final subtitleFile = SubtitleFile.fromFile(File('path_to_subtitle.srt'));
final subtitles = subtitleFile.subtitles;

9. 自定义 UI

你可以通过自定义 VideoSubtitleEditor 的 UI 来满足你的需求。例如,你可以添加一个时间轴控件来显示视频的进度,并允许用户通过拖动来调整字幕的时间。

10. 处理错误

在使用 video_subtitle_editor 插件时,可能会遇到各种错误,如视频加载失败、字幕格式错误等。你可以通过捕获异常来处理这些错误,并向用户显示友好的错误信息。

try {
  await _controller.loadVideo('path_to_your_video.mp4');
} catch (e) {
  print('Failed to load video: $e');
}
回到顶部
AI 助手
你好,我是IT营的 AI 助手
您可以尝试点击下方的快捷入口开启体验!