Video Recording and Replay with Flutter

In this tutorial I show you how to record and playback videos in Flutter.

Recording videos and preview the result can be easily done in Flutter. This tutorial shows how to create a record and replay flow using the camera and video_player packages.

Introduction

In this tutorial, we will use the camera and the video_player package to create a Flutter app flow to record and replay videos. The recorded video could then be used to – for example – upload it to a server.

First, we will start by creating a page which allows us to view the camera input and record a video.

Once the video has been recorded, we will open another page replaying the recorded video and allow the user to either accept or dismiss the video.

The source code of this tutorial is available on GitHub: https://github.com/bettercoding-dev/flutter-video.

Prerequisites

We first start with creating a new Flutter project.

Next, we clean up our pubspec.yaml file and add the necessary dependencies. After that, the content of pubspec.yaml should look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
name: flutter_video
description: Flutter Video Flow

publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 1.0.0+1

environment:
  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

You can see that we included the camera and the video_player packages.

iOS-specific prerequisites

To make the packages work in iOS we need to insert following code into ios/Runner/Info.plist:

1
2
3
4
5
6
7
8
9
<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>

Make sure to put this code within the existing dict block (before </dict>).

Android-specific prerequisites

We also need to modify one of the Android-specific files. To make the plugins work on Android, it is necessary to increase the minSdkVersion to 21. This can be done in the android/app/build.gradle file. Just search for minSdkVersion and replace the value. (Make sure you are not confusing android/build.gradle with android/app/build.gradle)

Don’t for get to run pub get after altering the pubspec.yaml file.

For our main.dart file, we will replace old its content with this code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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(),
    );
  }
}

Note that CameraPage does not exist yet, but we will create it in the next step.

Viewing Camera Input using the camera Plugin

Next, we are going to implement CameraPage that will show the input of the camera and lets us record a video. For this, let’s create a file called camera_page.dart and create a StatefulWidget. The stub of this file should look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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();
  }
}

Because there is some initialization going on before we can access the camera, we need to display some loading state while the initialization is in progress.

For this, we create a variable _isLoading in our _CameraPageState and check the state in the build function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);
    }
  }
}

We also added a CameraController variable and show a CameraPreview once the loading is done. Since this won’t work unless the _cameraController has been initialized, we continue with this step.

Initializing the CameraController

First, let’s create a function called _initCamera.

1
2
3
4
5
6
7
_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);
}
  • Line 2: Request all available cameras from the camera plugin.
  • Line 3: Selecting the front-facing camera.
  • Line 4: Create an instance of CameraController. We are using the CameraDescription of the front camera and setting the resolution of the video to the maximum.
  • Line 5: Initialize the controller with the set parameters.
  • Line 6: After the initialization, set the _isLoading state to false.

Finally, we call the _initCamera() function in initState().

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

When you now start the app, you should be able to see a preview of your front camera.

Recording Video

To start recording the video, we will add a record button. It should overlay the video preview, so we will put the CameraPreview and the button into a Stack widget.

Simply replace the content of the else branch with this code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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(),
        ),
      ),
    ],
  ),
);

Now, we need to create a new state variable _isRecording and set it to false.

1
bool _isRecording = false;

Finally, let’s create a _recordVideo() function to handle the button click. This function should look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
_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);
  }
}
  • Line 2: We need to handle both, start and stop recording actions. Therefore, we need to check if the video is currently recording.
  • Line 3: If the video is recording, we will stop it. The stopVideoRecording() function returns a file containing the recorded video.
  • Line 4: We update the state and set _isRecording to false.
  • Lines 5-9: Finally, with the recorded file, we will open the VideoPage to let the user check on the recorded video. (Note: VideoPage does not exist yet. It will be added in the next step)
  • Line 11: If the video is not yet recording, we tell the CameraController to prepare for recording.
  • Line 12: Once the preparation is done, we start the actual recording.
  • Line 13: Finally, we set the current state to _isRecording = true.

Replaying the Recorded Video

For replaying the recorded video, create a new file called video_page.dart and add a StatefulWidget, like we did for the CameraPage.

The VideoPage needs to accept a path to the recorded file. So, we add a new attribute filePath:

1
2
3
4
5
6
7
8
class VideoPage extends StatefulWidget {
  final String filePath;

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

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

For the _VideoPageState class, we start by creating a VideoController and a function to initialize it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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(BuildContext context) {
    return Container();
  }
}
  • Line 2: We add a new _videoPlayerController variable.
  • Lines 4-8: Don’t forget to dispose the VideoPlayerController once the State gets disposed.
  • Line 11: Create a new VideoPlayerController from the file path that we passed to this widget.
  • Line 12: Initialize the VideoPlayerController before we can start it.
  • Line 13: To play the video over and over again, we enable looping.
  • Line 14: Finally, we start the video.

As the last step, let’s define our UI. It should have an AppBar to accept or dismiss the video. And – of course – contain a VideoPlayer widget to replay the video. The build function for this widget could look like this:

 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
30
@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);
        }
      },
    ),
  );

}
  • Line 3: We use a Scaffold to create our page.
  • Lines 4-16: Add an AppBar that is transparent and includes an additional “OK” action. There, you could implement additional functionality to e.g. forward the video to a server.
  • Line 17: This parameter is set, to stretch the video behind the AppBar.
  • Lines 18-27: We use a FutureBuilder to show a CircularProgressIndicator as long as the initialization takes place. As soon as the initialization is done, the VideoPlayer is shown, replaying the video.

And, we are all done.

You can now start the app to record and replay videos.

Summary

In this tutorial, we used the camera and video_player packages to create a flow recording and replaying a video with Flutter.

The source code of this tutorial is available on GitHub: https://github.com/bettercoding-dev/flutter-video.

Built with Hugo
Theme based on Stack designed by Jimmy