My Journey building a Video Trimmer package for Flutter

Souvik Biswas
Flutter Community
Published in
8 min readMay 8, 2020

This started about 6 months back when I was working on a project which required a video editor for processing videos before uploading them to the backend server. The first thing that came to my mind was to search for a Flutter package for achieving this functionality. But, I got really upset on seeing that there wasn’t any package available for video editing on pub.dev.

At that point in time, I just started with Flutter app development and didn’t have the skills to build the whole editor myself. So, I decided to leave the project.

But even after 6 months, I was surprised to see that there was no package for video editing in Flutter, which comes with a good UI/UX as well. So, with the skills that I had gained over the past few months, I decided to build a video trimmer package for Flutter with a decent UI support.

You can find the video_trimmer package here:

Functionalities

The main functionalities of video trimmer are:

  • Retrieving and Storing video files to the File System in the specified format as selected by the user. The package supports most of the video formats which are widely used, like MP4, MKV, MOV, FLV, AVI, WVM as well as GIF for saving the output video.
  • Basic video playback controls.
  • Support for advanced FFmpeg custom commands.

Loading and Storing

The plugin supports most of the video formats as the input and the output trimmed video can be saved in formats: MP4, MKV, MOV, FLV, AVI, WVM as well as GIF. The formats can be selected from the class FileFormat.

If you want the output as a GIF then there are two additional options available:

  • fpsGIF: For setting an FPS value for the output GIF.
  • scaleGIF: To define the width of the output GIF, and the height is scaled accordingly as per the aspect ratio.

The trimmed video can be saved in either of the three directories defined in the StorageDir class. The directories are:

  • temporaryDirectory: Only accessible from inside the app, can be cleared at anytime
  • applicationDocumentsDirectory: Only accessible from inside the app
  • externalStorageDirectory: Supports only Android platform, accessible externally

By default, the trimmed videos are stored in the MP4 format inside a folder called Trimmer in the chosen directory, and the file name follows the format:

<original_file_name>_trimmed:<date_time>.<file_format>

For loading video:

final Trimmer _trimmer = Trimmer();
File _videoFile = await _trimmer.loadVideo();

Saving the trimmed video:

await _trimmer
.saveTrimmedVideo(startValue: _startValue, endValue: _endValue)
.then((value) {
setState(() {
_value = value;
});
});

You can also provide a custom folder name and a file name, which will be used to store the video file.

Video Playback Controls

This is a very simple method that returns a boolean value with the video playback state, that is, whether it is playing or paused. This can be useful if you want to trigger something in your app as soon as the playback state changes.

await _trimmer.videPlaybackControl(
startValue: _startValue,
endValue: _endValue,
);

Editing Videos

The package uses FFmpeg commands for trimming and defining the output format of the video.

The basic implementation of the FFmpeg command is as follows:

-i <path_to_video> -ss <start_point> -t <duration> -c copy <output_video_path>/<video_file_name>.<video_format>
  • -i: Used for providing the input video path
  • -ss: Used for seeking to the start point of the video
  • -t: Used for specifying the duration of the video
  • -c: Used for supplying a codec to the video
  • copy: A type of codec known as Stream copy mode, omits the decoding and encoding step for the specified stream. It helps in very fast and no quality loss conversion.

An example of trimming a video from the start point of 00:00:03 with the end point of 00:00:08 (i.e. the duration will be 00:00:05), and the video output format be .mp4 is given below:

-i /Photos/video.mp4 -ss 00:00:03 -t 00:00:05 -c copy /Photos/trim_video.mp4

For generating a GIF from the input video, a different FFmpeg command is used. It is as follows:

-i <path_to_video> -ss <start_point> -t <duration> -vf "fps=<fps_of_gif>,scale=<scale_of_gif>:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 <output_video_path>/<video_file_name>.gif
  • -vf: Used for supplying the number of frames to the output video
  • palettegen and paletteuse filters will generate and use a custom palette generated from your input.
  • -loop: Used for specifying the number of times the video will loop. A value of 0 is infinite looping, -1 is no looping, and any positive value will loop that number of times, meaning if the value is 3 it will play the video 4 times.

An example of trimming a video from the start point of 00:00:03 with the end point of 00:00:08 (i.e. the duration will be 00:00:05), and converting it to .gif (with fps of 10, scale of 480 & loop value of 0 , i.e. infinite looping) is given below:

-i /Photos/video.mp4 -ss 00:00:03 -t 00:00:05 -vf "fps=10,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 /Photos/trim_video.gif

The package also supports custom FFmpeg commands, check out the section below for more information.

Advanced commands

The package supports any FFmpeg commands with a user-defined output video format for editing the video. You will not need to use this custom command usually, as most of the video trimming features are already included in the package.

Refer to the Official FFmpeg Documentation for more information

NOTE: The advanced option does not provide any safety check, so if a wrong video format is passed in the app then that may result in a crash.

// Example of defining a custom command// This is already used for creating GIF by
// default, so you do not need to use this.
await _trimmer
.saveTrimmedVideo(
startValue: _startValue,
endValue: _endValue,
ffmpegCommand:
'-vf "fps=10,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0',
customVideoFormat: '.gif')
.then((value) {
setState(() {
_value = value;
});
});

UI/UX

Let’s come to the most interesting part now, the UI design of the video trimmer.

The images in the background have been generated from the video, at different positions by diving the whole video length into the number of parts to be displayed on the TrimEditor.

The two Text widgets placed in the two top corners show the start position and the end position of the trimmed video.

The Slider has been implemented using GestureDetector & CustomPaint widget.

GestureDetector

I have mainly used three callbacks of the GestureDetector widget:

  • onHorizontalDragStart
  • onHorizontalDragUpdate
  • onHorizontalDragEnd

The onHorizontalDragStart callback is executed as soon the pointer touches the screen at some position and just starts dragging horizontally. I have used it to sense whether the pointer is near to the start position or near to the end position and move the slider accordingly.

While the pointed is in motion the onHorizontalDragUpdate callback is active. Here, I have tried to give the painter a boundary so that it can’t slide beyond the start or the end point of the video. I also calculated the direction in which the slider start or end position can be dragged according to the position of the pointer, and determine whether the start point or the end point of the slider to be dragged.

And, the onHorizontalDragEnd callback is executed while the pointer is no longer touching the screen. I have just used it to set the Circular Holder (at the two ends of the slider) to normal size.

The first challenge that I faced here was that when the start and the end points of the slider are at the same location, then using the above method their motion wouldn’t be possible and they will remain stuck there forever. So, I had to write a different algorithm for their motion when they are close together.

The algorithms are not perfect and may have a few bugs, so anyone interested in improving them is welcome to make a PR to the GitHub repo.

CustomPaint

I have used the CustomPaint widget to draw the rectangular Slider, two Circular Holders, and the current video playback position.

For the Slider, I have just use the drawRect method on the canvas, passing the two-left coordinates and the bottom-right coordinates, to draw the rectangle.

var borderPaint = Paint()
..color = borderPaintColor
..strokeWidth = borderWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final rect = Rect.fromPoints(startPos, endPos);canvas.drawRect(rect, borderPaint);

For drawing the two circular holders:

canvas.drawCircle(
startPos + Offset(0, endPos.dy / 2), circleSize, circlePaint);
canvas.drawCircle(
endPos + Offset(0, -endPos.dy / 2), circleSize, circlePaint);

For the current video playback position:

canvas.drawLine(
currentPos,
currentPos + Offset(0, endPos.dy),
scrubberPaint,
);

The UI for the TrimEditor is completely customizable.

Conclusion

The package is currently in the beta stage and may contain few bugs. Feel free to contribute to the project on GitHub.

The GitHub repository link is below:

If you like this project, please give “Stars” (⭐️) to my GitHub repo.

Package Link:

Check out my other articles

If you want to support me and want me to continue writing Flutter articles and building some interesting Flutter projects, please contribute to my Patreon page below:

You can follow me on Twitter and find some of my projects on GitHub. Also, don’t forget to checkout my Website. Thank you for reading, if you enjoyed the article make sure to show me some love by hitting that clap (👏) button!

Happy coding…

Souvik Biswas

--

--

Souvik Biswas
Flutter Community

Mobile Developer (Android, iOS & Flutter) | Technical Writer (FlutterFlow) | IoT Enthusiast | Avid video game player