“Lights, Camera, Action: Building a Short Video Recorder App with Flutter and Bloc Pattern”
Hi there! Welcome to this blog where we’ll explore the exciting journey of creating a short video recorder app like tic-tok and Instagram using Flutter and Bloc.
By the time we’re done, you’ll have a complete app that empowers you to:
- Choose from flexible recording lengths (15, 30, 60, and 90 seconds).
- Experience press-and-hold recording with a sleek timer.
- Seamlessly switch between front and back cameras.
- Enjoy instant playback right after recording.
- Automatically off the camera when it’s not in use.
In this tutorial, our main focus will be on implementing the core camera functionality using the Bloc pattern. We’ll break down each event and state, explaining their purpose and interactions within the Bloc.
Note: If you’re not yet familiar with the Bloc pattern or Flutter’s camera implementation, consider checking out the flutter_bloc
and camera
on Pub.dev
.
Now, let’s dive right into the code and start building your very own video recording app!
1. Setting Up the Project
To begin, we must add the required dependency in your pubspec.yaml
file
2. Implementing Bloc
Defining Camera Events
We’ll start by defining the events. we have crafted a total of seven events, each serving a distinct purpose:
CameraInitialize
: Event to request permission and initialize the cameraCameraSwitch
: Event to switch between front and back cameras.CameraRecordingStart
: Event to start video recordingCameraRecordingStop
: Event to stop video recordingCameraEnable
: Event to enable the camera when in useCameraDisable
: Event to disable the camera when not in useCameraReset
: Event to reset the camera BLoC to its initial state
Defining Camera States
Based on the above events bloc can emit 4 states.
CameraInitial
: Represents the initial state when the camera has not been initialized.CameraReady
: Indicates the camera is initialized and ready for usage, along with flags for recording status, errors, and button deactivation.CameraRecordingSuccess
: Reflects the state when the video recording completes successfully, encapsulating the recorded video file.CameraError
: state when an error occurs during camera operations. This state includes aCameraErrorType
parameter that specifies the type of camera error that occurred.
enum CameraErrorType { permission, other }
Defining Camera Bloc
Now, let’s look into the CameraBloc
It’s equipped with two crucial dependencies — cameraUtils
and permissionUtils
– which we'll inject via the constructor this will help us in mocking the camera controller during the unit test.CameraUtils
and PermissionUtils
classes are responsible for initiating the camera controller and asking for camera permission.
The CameraBloc
will be the heart of our project, managing the camera-related states and events. This is where we'll handle camera initialization, switching between front and back cameras, starting and stopping video recording, and more.
Take an overview of the camera bloc class, we’ll break down each event and state in detail, explaining their purpose and how they interact to manage camera functionality effectively.
// A BLoC class that handles camera-related operations
class CameraBloc extends Bloc<CameraEvent, CameraState> {
//....... Dependencies ..............
final CameraUtils cameraUtils;
final PermissionUtils permissionUtils;
//....... Internal variables ........
int recordDurationLimit = 15;
CameraController? _cameraController;
CameraLensDirection currentLensDirection = CameraLensDirection.back;
Timer? recordingTimer;
ValueNotifier<int> recordingDuration = ValueNotifier(0);
//....... Getters ..........
CameraController? getController() => _cameraController;
bool isInitialized() => _cameraController?.value.isInitialized ?? false;
bool isRecording() => _cameraController?.value.isRecordingVideo ?? false;
//setters
set setRecordDurationLimit(int val) {
recordDurationLimit = val;
}
//....... Constructor ........
CameraBloc({required this.cameraUtils, required this.permissionUtils}) : super(CameraInitial()) {
on<CameraReset>(_onCameraReset);
on<CameraInitialize>(_onCameraInitialize);
on<CameraSwitch>(_onCameraSwitch);
on<CameraRecordingStart>(_onCameraRecordingStart);
on<CameraRecordingStop>(_onCameraRecordingStop);
on<CameraEnable>(_onCameraEnable);
on<CameraDisable>(_onCameraDisable);
}
}
- Event: CameraReset
The CameraReset
event is triggered to reset the CameraBloc
to its initial state. Inside the _onCameraReset
method, we handle the reset logic.
mainly we will be using it when we would be disposing of the camera page.
// Handle CameraReset event
void _onCameraReset(CameraReset event, Emitter<CameraState> emit) async {
await _disposeCamera(); // Dispose of the camera before resetting
_resetCameraBloc(); // Reset the camera BLoC state
emit(CameraInitial()); // Emit the initial state
}
2. Event: CameraInitialize
The CameraInitialize
event is triggered when the camera needs to be initialized. This event includes an optional recordingLimit
parameter that specifies the maximum duration of a video recording.
Inside the _onCameraInitialize
method, we manage the initialization process. We check camera permissions and handle camera setup. If permission is granted, we initialize the camera and emit the CameraReady
state. In case of an error, we emit the appropriate CameraError
state.
// Handle CameraInitialize event
void _onCameraInitialize(CameraInitialize event, Emitter<CameraState> emit) async {
recordDurationLimit = event.recordingLimit;
try {
await _checkPermissionAndInitializeCamera(); // checking and asking for camera permission and initializing camera
emit(CameraReady(isRecordingVideo: false));
} catch (e) {
emit(CameraError(error: e == CameraErrorType.permission ? CameraErrorType.permission : CameraErrorType.other));
}
}
The _checkPermissionAndInitializeCamera
method is a critical step in this process. It checks for camera and microphone permissions, initializes the camera if permissions are granted, and handles permission requests if not.
// Check and ask for camera permission and initialize camera
Future<void> _checkPermissionAndInitializeCamera() async {
if (await permissionUtils.getCameraAndMicrophonePermissionStatus()) {
await _initializeCamera();
} else {
if (await permissionUtils.askForPermission()) {
await _initializeCamera();
} else {
return Future.error(CameraErrorType.permission); // Throw the specific error type for permission denial
}
}
}
The _initializeCamera
method, on the other hand, is responsible for setting up the camera controller and handling any potential exceptions. We listen to the camera controller for recording status changes and start a timer to keep track of the recording duration.
// Initialize the camera controller
Future<void> _initializeCamera() async {
_cameraController = await cameraUtils.getCameraController(lensDirection: currentLensDirection);
try {
await _cameraController?.initialize();
_cameraController?.addListener(() {
if (_cameraController!.value.isRecordingVideo) {
_startTimer();
}
});
} on CameraException catch (error) {
Future.error(error);
} catch (e) {
Future.error(e);
}
}
// Start the recording timer
void _startTimer() async {
recordingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
recordingDuration.value++;
if (recordingDuration.value == recordDurationLimit) {
add(CameraRecordingStop());
}
});
}
// Stop the recording timer and reset the duration
void _stopTimerAndResetDuration() async {
recordingTimer?.cancel();
recordingDuration.value = 0;
}
3. Event: CameraSwitch
The CameraSwitch
event is triggered to switch between the front and back cameras.
Within the _onCameraSwitch
method, we initiate the camera switch. We emit the CameraInitial
state to reset the camera view, then proceed with the camera switch operation. Subsequently, we emit the CameraReady
state to indicate that the camera is now ready for use.
// Handle CameraSwitch event
void _onCameraSwitch(CameraSwitch event, Emitter<CameraState> emit) async {
emit(CameraInitial()); // Reset camera view
await _switchCamera(); // Switch the camera
emit(CameraReady(isRecordingVideo: false)); // Camera is ready
}
The _switchCamera
method manage the camera switch operation. It toggles between the front and back camera lenses and reinitializes the camera setup to reflect the change.
// Switch between front and back cameras
Future<void> _switchCamera() async {
currentLensDirection = currentLensDirection == CameraLensDirection.back ? CameraLensDirection.front : CameraLensDirection.back;
await _reInitialize();
}
The _reInitialize
the method takes care of reinitializing the camera setup. It disposes of the existing camera controller and then initializes it again with the new lens direction configuration.
// Reinitialize the camera
Future<void> _reInitialize() async {
await _disposeCamera();
await _initializeCamera();
}
4. Event: CameraRecordingStart
The CameraRecordingStart
event, a crucial element that initiates video recording within our app.
In the method _onCameraRecordingStart
, we respond to the user's intent to begin recording. If the camera is not already recording, we emit the CameraReady
state with the isRecordingVideo
flag set to true. Subsequently, we trigger the _startRecording
method to commence the recording process. By emitting CameraReady
just before the recording start we are ensuring that the user doesn’t see any lag on the UI as _cameraController!.startVideoRecording()
is a future that may take some time to complete.
// Handle CameraRecordingStart event
void _onCameraRecordingStart(CameraRecordingStart event, Emitter<CameraState> emit) async {
if (!isRecording()) {
try {
emit(CameraReady(isRecordingVideo: true));
await _startRecording();
} catch (e) {
await _reInitialize();
emit(CameraReady(isRecordingVideo: false));
}
}
}
The _startRecording
method is responsible for starting the video recording process. It calls the _cameraController!.startVideoRecording()
method to begin recording. In case of any exceptions, we handle the error and reinitialize the camera.
Future<void> _startRecording() async {
try {
await _cameraController!.startVideoRecording();
} catch (e) {
return Future.error(e);
}
}
5. Event: CameraRecordingStop
The CameraRecordingStop
event is triggered when the user stops video recording.
Inside the _onCameraRecordingStop
method, we manage the process of stopping video recording. If the camera is currently recording, we check whether the recorded video duration is less than 2 seconds to prevent corrupt file issues with very short videos. We emit the CameraReady
state accordingly, signaling the recording status and potential errors.
// Handle CameraRecordingStop event
void _onCameraRecordingStop(CameraRecordingStop event, Emitter<CameraState> emit) async {
if (isRecording()) {
// Check if the recorded video duration is less than 2 seconds to prevent
// potential issues with very short videos resulting in corrupt files.
bool hasRecordingLimitError = recordingDuration.value < 2 ? true : false;
emit(CameraReady(isRecordingVideo: false, hasRecordingError: hasRecordingLimitError, decativateRecordButton: true));
File? videoFile;
try {
videoFile = await _stopRecording(); // Stop video recording and get the recorded video file
if (hasRecordingLimitError) {
await Future.delayed(
const Duration(milliseconds: 1500), () {}); // To prevent rapid consecutive clicks, we introduce a debounce delay of 2 seconds,
emit(CameraReady(isRecordingVideo: false, hasRecordingError: false, decativateRecordButton: false));
} else {
emit(CameraRecordingSuccess(file: videoFile));
}
} catch (e) {
await _reInitialize(); // On Camera Exception, initialize the camera again
emit(CameraReady(isRecordingVideo: false));
}
}
}
The _stopRecording
method plays a crucial role in halting video recording. It calls _cameraController!.stopVideoRecording()
to stop recording, retrieves the recorded video file, and manages the timer.
// Stop video recording and return the recorded video file
Future<File> _stopRecording() async {
try {
XFile video = await _cameraController!.stopVideoRecording();
_stopTimerAndResetDuration();
return File(video.path);
} catch (e) {
return Future.error(e);
}
}
6. Event: CameraDisable and CameraEnable
The CameraDisable
and CameraEnable
events are triggered to disable and enable the camera, respectively when the camera is not in use.
In the _onCameraEnable
method, we handle the event when the camera needs to be re-enabled, typically when the app resumes from the background. If the camera is not initialized and a camera controller instance exists, we check for camera and microphone permissions. If granted, we proceed to initialize the camera and emit the CameraReady
state. If permissions are not granted, we emit the CameraError
state with the appropriate error type.
// Handle CameraEnable event on app resume
void _onCameraEnable(CameraEnable event, Emitter<CameraState> emit) async {
if (!isInitialized() && _cameraController != null) {
if (await permissionUtils.getCameraAndMicrophonePermissionStatus()) {
await _initializeCamera();
emit(CameraReady(isRecordingVideo: false));
} else {
emit(CameraError(error: CameraErrorType.permission));
}
}
}
In the _onCameraDisable
method, we respond to the event of disabling the camera, typically when the camera is not in active use. If the camera is initialized and currently recording, we first stop the recording to ensure that any ongoing recording session is concluded properly. We introduce a brief delay after stopping recording to handle scenarios where the app is minimized during recording. Afterward, we dispose of the camera, emit the CameraInitial
state to reset the camera setup, and signify that the camera is not in use.
// Handle CameraDisable event when camera is not in use
void _onCameraDisable(CameraDisable event, Emitter<CameraState> emit) async {
if (isInitialized() && isRecording()) {
// if app minimize while recording then save the the video then disable the camera
add(CameraRecordingStop());
await Future.delayed(const Duration(seconds: 2));
}
await _disposeCamera();
emit(CameraInitial());
}
Putting It All Together
The CameraBloc
logic, as explained above, forms the core of our camera management system. By handling different events and emitting corresponding states, we ensure that the camera-related functionality is properly managed and presented to the user.
In the next part of our exploration, we’ll shift our focus to the UI components and widgets that interact with the CameraBloc
to create a seamless user experience for camera recording and playback.
3. User Interface: Building the CameraPage
In this section, we will only talk about key components and their interactions with the CameraBloc
.
The CameraPage
is the main screen where users can interact with the camera. It provides the camera preview, recording controls, camera switching, and timer settings.
Initialization
Inside the CameraPage
initState
, we initialize the CameraBloc
and add an observer to handle app lifecycle changes. This ensures that the camera is enabled when the app is resumed and visible, and disabled when the app is inactive or not visible.
The VisibilityDetector
widget wraps the entire CameraPage
and detects changes in visibility. When the page becomes invisible, we disable the camera, and when it becomes visible again, we enable the camera.
VisibilityDetector(
key: const Key("my_camera"),
onVisibilityChanged: _handleVisibilityChanged,
child: BlocConsumer<CameraBloc, CameraState>(
listener: _cameraBlocListener,
builder: _cameraBlocBuilder,
),
),
The _handleVisibilityChanged
method updates the camera's state based on visibility changes.
void _handleVisibilityChanged(VisibilityInfo info) {
if (info.visibleFraction == 0.0) {
// Camera page is not visible, disable the camera.
if (mounted) {
cameraBloc.add(CameraDisable());
isThisPageVisible = false;
}
} else {
// Camera page is visible, enable the camera.
isThisPageVisible = true;
cameraBloc.add(CameraEnable());
}
}
Let’s dive deeper into the CameraPage
's _cameraBlocListener
and _cameraBlocBuilder
methods to understand their roles in handling camera events and building the UI.
_cameraBlocListener
The _cameraBlocListener
method is responsible for reacting to changes CameraState
and taking appropriate actions based on those changes. Here's the breakdown of what's happening within this method:
- Recording Success: If the current
CameraState
isCameraRecordingSuccess
, it means that a video recording has been successfully completed. In this case, we navigate to theVideoPage
, passing the recorded video file to the constructor. TheVideoPage
will handle the playback of this video. - Recording Error: If the current
CameraState
isCameraReady
and thehasRecordingError
flag istrue
, it indicates that there was an error during video recording, likely due to recording for less than 2 seconds. We show a snack bar to notify the user to record for a longer duration.
_cameraBlocBuilder
The _cameraBlocBuilder
method constructs the user interface based on the current CameraState
. It handles everything from displaying the camera preview to showing error messages and recording controls.
AnimatedSwitcher
is used to toggle between showing the live camera preview and displaying a blurred screenshot image as a transition while the camera is initializing or switching between cameras. This is done to provide a smoother visual experience to the user and avoid sudden flickers or black screens during these transitions. This screenshot serves as a temporary visual placeholder to avoid showing a black screen during the transition.
The takeCameraScreenshot
function is a utility that allows you to capture a screenshot of any widget in your Flutter app. In the context of the, it is specifically used to capture a screenshot of the camera preview
The animatedProgressButton
widget is used to display the recording controls. The UI responds to tap and long-press gestures, allowing the user to start and stop video recording. The UI also includes buttons for switching between cameras and adjusting the timer settings.
The RecordingProgressIndicator
is a custom widget that visually represents the progress of a video recording. It displays a radial gauge-like design that fills up as the video recording progresses.
If the current CameraState
is CameraError
, the errorWidget
the function is called to display an error message. This message informs the user that camera and microphone permissions are needed to proceed.
4. Playback: Building the VideoPage
The VideoPage
is a simple page that displays a video player for playback. This page utilizes the VideoPlayer
package to create a seamless and interactive playback experience.
5. Conclusion
🎉 Congratulations!, we have successfully completed building a Short Video Recorder App using Flutter and Bloc. 📹 We’ve explored essential components like CameraBloc, CameraPage, and VideoPage, gaining insights into camera management, video recording, and playback. You now have a solid foundation to create your unique short video app, adding your own creative features and ideas. 🚀
🎉 Thank you for reading this article! 🙏 You can find the source code on this GitHub repository.
💻 Feel free to contribute, share your improvements, or use it as a reference for your projects.
If you loved it, give it a clap! 👏
you can connect with me on LinkedIn and Twitter for more amazing content! 🚀
Keep coding and stay curious! 🚀 Happy Fluttering! 😊