原文链接:Tutorial: Video Recording and Replay with Flutter - 原文作者 Stefan

本文采用意译的方式

Flutter 中,视频录制和预览视频很容易做到。这个教程为我们展示怎么使用 cameravideo_player 包来创建录制和播放回放视频。

介绍

在这个教程中,我们将使用 cameravideo_player 包来创建带有视频录制和视频回放功能的Flutter 应用。记录下来的视频可以被使用 - 比如,上传到远程服务器。

首先,我们将创建一个页面,用来展示摄像头的输入和录制一个视频

在视频录制之后,我们将打开另外一个页面来回放视频,这允许用户观看或者关闭视频。

本教程的相关代码放在 GitHub 的 github.com/bettercodin…

前置条件

首先,我们创建一个新的 Flutter 项目

接下来,我们将清空 pubspec.yaml 文件,然后添加必要的依赖pubspec.yaml 文件内容应该如下:

name: flutter_video
description: Flutter Video Flow

publish_to: 'none' # 如果我们希望发布到 pub.dev ,则移除该行

version: 1.0.0+1

enviroment:
  sdk: ">=2.12.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  camera: ^0.9.4+3
  video_player: ^2.2.6

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^1.0.0

flutter
  uses-material-design: true
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

正如你所看到,我们添加了包 cameravideo_player

iOS 专属前置条件

为了让包能够在 iOS 中工作,我们需要在文件 ios/Runner/Info.plist 文件中,插入下面的内容:

<key>NSCameraUsageDescription</key>
<string>Needed for recording videos.</string> <key>NSMicrophoneUsageDescription</key>
<string>Needed for recording videos.</string> <key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

确保在 dict 代码块中(在 </dict>) 之前添加这些代码。

Android 专属前置条件

我们也需要更改 Android 专属文件。为了让插件在 Android 上工作。我们需要将 minSdkVersion 升到 21。我们在 android/app/build.gradle 文件中完成。如果我们只是搜索 minSdkVersion 关键字来替换,请区分 android/build.gradleandroid/app/build.gradle

在更改 pubspec.yaml 文件之后,记得运行 pub get 命令行。

在我们的 main.dart 文件中,我们将用下面的代码来替换旧的代码。

import 'package:flutter/material.dart';
import 'package:flutter_video/camera_page.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: CameraPage(),
    );
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

需要注意的是,现在 CameraPage 还不存在,但是我们接下来会创建它。

使用 camera 插件展示摄像头输入

接下来,我们将实现 CameraPage 页面来展示摄像头引入和我们怎么使用来录制视频。为了实现这个,我们创建一个名为 camera_page.dart 的文件,然后创建一个 StatefulWidget。如下:

import 'package:flutter/material.dart';

class CameraPage extends StatefulWidget {
  const CameraPage({Key? key}) : super(key: key);

  @override
  _CameraPageState createState() => _CameraPageState();
}

class _CameraPageState extends State<CameraPage> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

因为在我们访问相机之前需要进行一些初始化,我们需要在初始化过程中显示一些加载状态。

为了实现这个,我们在 _CameraPageState 中创建一个变量 _isLoading,并在 build 函数中检查该状态。

class _CameraPageState extends State<CameraPage> {
  bool _isLoading = true;
  late CameraController _cameraController;

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

  @override
  Widget build(BuildContext context) {
    if(_isLoading) {
      return Container(
        color: Colors.white,
        child: const Center(
          child: CircularProgressIndicator(),
        ),
      );
    } else {
      return CameraPreview(_cameraController);
    }
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

我们也添加了类型为 CameraController 变量。然后在加载完成之后,展示 CameraPreview。除非 _cameraController 已初始化,否则这将不起作用,还是会显示加载这步。

初始化 CameraController

首先,我们创建一个名为 _initCamera 的函数。

_initCamera() async {
  final cameras = await availableCameras();
  final front = cameras.firstWhere((camera) => camera.lensDirection == CameraLensDirection.front);
  _cameraController = CameraController(front, ResolutionPreset.max);
  await _cameraController.initialize();
  setState(() => _isloading = false);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 第 2 行代码:从 camera 插件中请求所有可以获取到的摄像头
  • 第 3 行代码:选择前置摄像头
  • 第 4 行代码:创建一个 CameraController 实例。我们使用前置摄像头的 CameraDescription 然后设置视频的分辨率到最大
  • 第 5 行代码:设置参数初始化控制器
  • 第 6 行代码:在初始化之后,设置 _isLoading 的值为 false

最后,我们在 initState() 中调用 _initCamera() 函数。

@override
void initState() {
  super.initState();
  _initCamera();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

至此,当我们开启应用,我们应该可以看到前置摄像头的预览。

录制视频

为了开启录制视频,我添加一个录制按钮。这个按钮应该在视频的上层,所以,我们将 CameraPreview 和这个按钮在 Stack 挂件中。

简单替换 else 分支的内容为如下代码:

return Center(
  child: Stack(
    alignment: Alignment.bottomCenter,
    children: [
      CameraPreview(_cameraController),
      Padding(
        padding: const EdgeInsets.all(25),
        child: FloatingActionButton(
          backgroundColor: Colors.red,
          child: Icon(_isRecording ? Icons.stop : Icons.circle),
          onPressed: () => _recordVideo(),
        ),
      ),
    ],
  ),
);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

现在,我们需要创建一个新的状态变量 _isRecording 并赋值为 false

bool _isRecording = false;
  • 1.

最后,我们创建一个 _recordVideo() 函数来处理按钮的点击。这个函数如下:

_recordVideo() async {
  if (_isRecording) {
    final file = await _cameraController.stopVideoRecording();
    setState(() => isRecording = false);
    final route = MaterialPageRoute(
      fullscreenDialog: true,
      builder: (_) => VideoPage(filePath: file.path),
    );
    Navigator.push(context, route);
  } else {
    await _cameraController.prepareForVideoRecording();
    await _cameraController.startVideoRecording();
    setState(() => _isRecording = true);
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 第 2 行代码:我们需要处理开始和停止记录的行为,因此我们需要检查视频当前的记录的状态。
  • 第 3 行代码:如果视频正在录制,我们将暂停它。stopVideoRecording() 函数返回一个包含录制视频的文件。
  • 第 4 行代码:我们更新 _isRecording 的状态为 false
  • 第 5-9 行代码:最后,获得了录制的视频文件,我们将打开 VideoPage,然后让用户检查录制的视频。(备注: VideoPage 目前还不存在,我们稍后会添加)
  • 第 11 行代码:如果视频还没有录制,我们将告诉 CameraController 准备录制。
  • 第 12 行代码:一旦准备完毕,我们将开启记录
  • 第 13 行代码:最后,我们设置 _isRecording 的状态为 true

回放录制的视频

为了回放录制视频,创建一个新的文件,名为 video_page.dart,然后添加一个 StatefulWidget,正如我们在 CameraPage 所做的那样。

VideoPage 需要接受录制视频文件路径。所以,我们添加一个新的属性 filePage:

class VideoPage extends StatefulWidget {
  final String filePath;

  const VideoPage({ Key? key, required this.filePath }): super(key: key);

  @override
  _VideoPageState createState() => _VideoPageState();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

_VideoPageState 类中,我们通过创建一个 VideoController 开始,和一个函数来初始化它。

class _VideoPageState extends State<VideoPage> {
  late VideoPlayerController _videoPlayerController;

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

  Future _initVideoPlayer() async {
    _videoPlayerController = VideoPlayerController.file(File(widget.filePath));
    await _videoPlayerController.initialize();
    await _videoPlayerController.setLooping(true);
    await _videoPlayerController.play();
  }

  @override
  Widget build(BuilderContext context) {
    return Container();
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 第 2 行代码:我们添加了一个 _videoPlayerController 变量
  • 第 4-8 行代码:一旦 State 已经被销毁了,别忘记销毁 VideoPlayerController
  • 第 11 行代码:从我们传递给挂件的文件路径,来创建一个新的 VideoPlayerController
  • 第 12 行代码:在我们可以调用之前,初始化 VideoPlayerController
  • 第 13 行代码:为了能够重复播放视频,我们开启循环
  • 第 14 行代码:最后,开启视频

最后一步,我们来设计自己的 UI。我们应该有一个 AppBar 来进入或者销毁视频。嗯,然后,包含一个 VideoPlayer 挂件来回放视频。这个挂件中的 build 函数可能像这样:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('Preview'),
      elevation: 0,
      backgroundColor: Colors.black26,
      actions: [
        IconButton(
          icon: const Icon(Icons.check),
          onPressed: () {
            print('do something with the file');
          },
        ),
      ]
    ),
    extendBodyBehindAppBar: true,
    body: FutureBuilder(
      future: _initVideoPlayer(),
      builder: (context, state) {
        if(state.connectionState == ConnectionState.waiting) {
         return const Center(child: CircularProgressIndicator());
        } else {
          return VideoPlayer(_videoPlayerController);
        }
      }
    ),
  );
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 第 3 行代码:我们使用 Scafflod 来创建我们的页面
  • 第 4-16 行代码:添加一个透明的 AppBar,并包含额外的 OK 操作。在这里,我们可以实现额外的功能,比如从服务端获取视频。
  • 第 17 行代码:这个参数是设置,是为了拉伸视频到 AppBar 后面
  • 第 18-27 行代码:我们使用 FutureBuilder ,当正在初始化,则显示 CircularProgressIndicator。当初始化完成,则显示 VideoPlayer, 并播放回放视频

嗯,至此,我们已经全部完成。

我们现在可以启动应用,来录制并回放视频。

Flutter 中的视频录制和回放_初始化

总结

在这个教程中,在 Flutter 中,我们使用 cameravideo_player 包来创建一个录制和回放视频的流程。