How to Use FFmpeg in Android with Example?
FFmpeg, short for Fast-forward MPEG, is a free and open-source multimedia framework, which is able to decode, encode, transcode, mux, demux, stream, filter and play fairly all kinds of multimedia files that have been created to date. It also supports some of the eldest formats. FFmpeg compiles and runs, across various operating systems like Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. among a large range of build environments, machine architectures, and configurations. Programming languages used in FFmpeg are C and Assembly language. We can do many fun kinds of stuff using Ffmpeg like, Video Compress, Audio Compress, Trim Video, Rotate Video, Crop Video, Adding filters to videos, Reverse a Video, Creating fast and slow-motion video, Fade in fade out, Merge audio and video, Creating a video from images, Convert video from one format into another, Extract Picture from Video or Sound from Video, Gifs overlay, and many more. FFmpeg is part of the workflow of hundreds of other media-related software projects, and it’s often used behind the scenes. Also, It is an internal part of software such as VLC media player, YouTube, Plex, iTunes, Shortcut, Blender, Kodi, HandBrake, it handles video and audio playback in Google Chrome, and Linux version of Firefox. FFmpeg comprises an enormous set-up of libraries and projects for dealing with video, sound, and other multimedia files and streams.
FFmpeg Libraries
- libavutil is a utility library to help versatile media programming. It contains portable string functions, arbitrary number generators, extra arithmetic capacities, data structures, cryptography, and core multimedia utilities.
- libavcodec is a library that provides encoders and decoders for video/audio codecs, subtitle streams, and several bitstream channels.
- libavformat is a library that provides multiplexing and demultiplexing framework for video/audio codecs, subtitle streams
- libavdevice is a library containing I/O devices for getting from and delivering to numerous multimedia I/O programming systems, including Video4Linux, ALSA, and VfW.
- libavfilter library provides a media filtering framework that contains several filters and sinks.
- libswscale library performs exceptionally enhanced picture scaling and pixel format transformation tasks.
- libswresample is a library that performs highly optimized but a lossy change in audio rate, change in channel layout, for example from stereo to mono, and sample format conversion operations.
Android doesn’t have efficient and robust APIs for multimedia which could provide functionalities like FFmpeg. The only API which android has is MediaCodec API, but it is faster than FFmpeg because it uses the device hardware for video processing.
Create a Small Video Editor App in Android Studio using FFmpeg
Prerequisites:
Before we start, we need to set up an environment to run our FFmpeg commands. There are two options to do so:
- By building our own library
- By using any compiled source provided by the community. There are many libraries that can be used to perform FFmpeg operations in Android. For example,
- WritingMinds
- Bravobit
- tanersener/mobile-ffmpeg
- yangjie10930/EpMedia: There are many inbuilt functions present in this library from which you can clip, crop, rotate, add a logo, add a custom filter, merge different videos.
Although it is highly recommended to build your library, because that will reduce your apk size, you can add a third-party library and can update the library with time as you want. But, this process is very time consuming and requires extra skills. So, as a beginner, you can use some above-mentioned libraries and if you face some issue you can raise that issue on their respective GitHub repository. In the below example I will be using tanersener/mobile-ffmpeg, as it has support for Android 10 scoped storage, and also it is the best library available on the internet for FFmpeg mobile. A sample GIF is given below to get an idea about what we are going to do in this article. Note that we are going to implement this project using the Java language.
Step by Step Implementation
Step 1: Create a New Project
To create a new project in Android Studio please refer to How to Create/Start a New Project in Android Studio. Note that select Java as the programming language.
Step 2: Adding a dependency to the build.gradle file
We will be using tanersener/mobile-ffmpeg library to implement FFmpeg functionalities in our app. And we will also require a rangeseekbar to select the particular length of the video. So add these dependencies in build.gradle file.
- implementation ‘com.arthenica:mobile-ffmpeg-full:4.4’
- implementation ‘org.florescu.android.rangeseekbar:rangeseekbar-library:0.3.0’
Step 3: Working with the colors.xml file
Below is the code for the colors.xml file.
XML
<? xml version = "1.0" encoding = "utf-8" ?> < resources > < color name = "colorPrimary" >#3F51B5</ color > < color name = "colorPost" >#0091EA</ color > < color name = "colorPrimaryDark" >#303F9F</ color > < color name = "colorAccent" >#E25E14</ color > < color name = "colorAccentTrans" >#BEE25E14</ color > < color name = "maincolor" >#E25E14</ color > < color name = "yellow" >#feeb3c</ color > < color name = "white" >#fff</ color > < color name = "semitranswhitecolor" >#00FFFFFF</ color > < color name = "colorwhite_50" >#CCffffff</ color > < color name = "colorwhite_10" >#1Affffff</ color > < color name = "colorwhite_30" >#4Dffffff</ color > < color name = "black" >#2F2F2F</ color > < color name = "graycolor" >#D3D3D3</ color > < color name = "graycolor2" >#C5C4C4</ color > < color name = "gainsboro" >#DCDCDC</ color > < color name = "lightgraycolor" >#f2f2f2</ color > < color name = "darkgray" >#93959A</ color > < color name = "darkgraytrans" >#9493959A</ color > < color name = "darkgraytransPost" >#9BC5C6C9</ color > < color name = "dimgray" >#696969</ color > < color name = "lightblack" >#5d5d5d</ color > < color name = "delete_message_bg" >#f2f2f2</ color > < color name = "delete_message_text" >#b8b8b8</ color > < color name = "transparent" >#00ffffff</ color > < color name = "fifty_transparent_black" >#802F2F2F</ color > < color name = "redcolor" >#ff0008</ color > < color name = "semitransredcolor" >#93FF0008</ color > < color name = "semitransredcolornew" >#918E8E</ color > < color name = "color_gray_alpha" >#65b7b7b7</ color > < color name = "app_blue" >#0e1f2f</ color > < color name = "text_color" >#000000</ color > < color name = "seekbar_color" >#3be3e3</ color > < color name = "line_color" >#FF15FF00</ color > < color name = "shadow_color" >#00000000</ color > < color name = "app_color" >#c52127</ color > </ resources > |
Step 4: Working with the activity_main.xml file
Go to the activity_main.xml file and refer to the following code. Below is the code for the activity_main.xml file.
XML
<? xml version = "1.0" encoding = "utf-8" ?> < RelativeLayout android:layout_width = "match_parent" android:layout_height = "match_parent" android:background = "#43AF47" tools:context = ".MainActivity" > < RelativeLayout android:id = "@+id/relative1" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:layout_alignParentTop = "true" android:layout_margin = "10dp" > < Button android:id = "@+id/cancel_button" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_centerInParent = "true" android:background = "@color/transparent" android:text = "Select Video" android:textColor = "@color/white" /> </ RelativeLayout > < VideoView android:id = "@+id/layout_movie_wrapper" android:layout_width = "match_parent" android:layout_height = "match_parent" android:layout_above = "@+id/relative" android:layout_below = "@+id/relative1" /> < TextView android:id = "@+id/progressbar" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_centerInParent = "true" android:paddingBottom = "5dp" /> < RelativeLayout android:id = "@+id/imagelinear" android:layout_width = "match_parent" android:layout_height = "match_parent" android:layout_above = "@+id/relative" android:layout_below = "@+id/relative1" android:layout_centerInParent = "true" > < TextView android:id = "@+id/overlaytextview" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_centerInParent = "true" android:text = "Raghav" android:textAppearance = "@style/TextAppearance.AppCompat.Medium" android:textColor = "@color/white" android:textStyle = "bold" android:visibility = "gone" /> < ImageView android:id = "@+id/overlayimage" android:layout_width = "match_parent" android:layout_height = "match_parent" android:layout_centerInParent = "true" android:scaleType = "fitXY" /> </ RelativeLayout > < LinearLayout android:id = "@+id/relative" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:layout_alignParentBottom = "true" android:orientation = "vertical" > < RelativeLayout android:layout_width = "match_parent" android:layout_height = "wrap_content" > < TextView android:id = "@+id/textleft" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_alignParentStart = "true" android:layout_marginBottom = "10dp" android:text = "00:00" android:textColor = "@color/white" /> < TextView android:id = "@+id/textright" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_alignParentEnd = "true" android:layout_marginBottom = "10dp" android:layout_weight = "1" android:text = "00:00" android:textAlignment = "textEnd" android:textColor = "@color/white" /> </ RelativeLayout > < RelativeLayout android:layout_width = "match_parent" android:layout_height = "wrap_content" android:background = "@color/white" > < org.florescu.android.rangeseekbar.RangeSeekBar android:id = "@+id/rangeSeekBar" android:layout_width = "match_parent" android:layout_height = "wrap_content" app:activeColor = "@color/white" app:alwaysActive = "true" app:barHeight = "2dp" app:showLabels = "false" app:textAboveThumbsColor = "#000000" /> </ RelativeLayout > < LinearLayout android:layout_width = "match_parent" android:layout_height = "10dp" /> < RelativeLayout android:layout_width = "match_parent" android:layout_height = "wrap_content" > < LinearLayout android:id = "@+id/lineartime" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:orientation = "vertical" > < TextView android:id = "@+id/text" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginBottom = "10dp" android:text = "" android:textColor = "@color/semitransredcolornew" /> < LinearLayout android:layout_width = "match_parent" android:layout_height = "wrap_content" android:orientation = "horizontal" > < LinearLayout android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_margin = "10dp" android:layout_weight = "1" android:orientation = "vertical" > < ImageButton android:id = "@+id/slow" android:layout_width = "50dp" android:layout_height = "50dp" android:layout_gravity = "center" android:background = "@color/transparent" android:scaleType = "fitXY" android:src = "@drawable/icon_effect_slow" /> < TextView android:layout_width = "match_parent" android:layout_height = "wrap_content" android:text = "Slow Motion" android:textAlignment = "center" android:textColor = "@color/white" /> </ LinearLayout > < LinearLayout android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_margin = "10dp" android:layout_weight = "1" android:orientation = "vertical" > < ImageButton android:id = "@+id/reverse" android:layout_width = "50dp" android:layout_height = "50dp" android:layout_gravity = "center" android:background = "@color/transparent" android:scaleType = "fitXY" android:src = "@drawable/icon_effect_time" /> < TextView android:layout_width = "match_parent" android:layout_height = "wrap_content" android:text = "Reverse" android:textAlignment = "center" android:textColor = "@color/white" /> </ LinearLayout > < LinearLayout android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_margin = "10dp" android:layout_weight = "1" android:orientation = "vertical" > < ImageButton android:id = "@+id/fast" android:layout_width = "50dp" android:layout_height = "50dp" android:layout_gravity = "center" android:background = "@color/transparent" android:scaleType = "fitXY" android:src = "@drawable/icon_effect_repeatedly" /> < TextView android:layout_width = "match_parent" android:layout_height = "wrap_content" android:text = "Flash" android:textAlignment = "center" android:textColor = "@color/white" /> </ LinearLayout > </ LinearLayout > </ LinearLayout > < LinearLayout android:id = "@+id/lineareffects" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:orientation = "vertical" > < TextView android:id = "@+id/text2" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginBottom = "10dp" android:text = "Tap to add effects" android:textColor = "@color/white" /> </ LinearLayout > </ RelativeLayout > </ LinearLayout > </ RelativeLayout > |
Step 5: Working with the MainActivity.java file
Go to the MainActivity.java file and refer to the following code. Below is the code for the MainActivity.java file. Comments are added inside the code to understand the code in more detail.
Java
import android.app.ProgressDialog; import android.content.ContentValues; import android.content.Intent; import android.media.MediaPlayer; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.provider.MediaStore; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; import android.widget.VideoView; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.arthenica.mobileffmpeg.Config; import com.arthenica.mobileffmpeg.ExecuteCallback; import com.arthenica.mobileffmpeg.FFmpeg; import org.florescu.android.rangeseekbar.RangeSeekBar; import java.io.File; import static com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL; import static com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS; public class MainActivity extends AppCompatActivity { private ImageButton reverse, slow, fast; private Button cancel; private TextView tvLeft, tvRight; private ProgressDialog progressDialog; private String video_url; private VideoView videoView; private Runnable r; private RangeSeekBar rangeSeekBar; private static final String root = Environment.getExternalStorageDirectory().toString(); private static final String app_folder = root + "/GFG/" ; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); rangeSeekBar = (RangeSeekBar) findViewById(R.id.rangeSeekBar); tvLeft = (TextView) findViewById(R.id.textleft); tvRight = (TextView) findViewById(R.id.textright); slow = (ImageButton) findViewById(R.id.slow); reverse = (ImageButton) findViewById(R.id.reverse); fast = (ImageButton) findViewById(R.id.fast); cancel = (Button) findViewById(R.id.cancel_button); fast = (ImageButton) findViewById(R.id.fast); videoView = (VideoView) findViewById(R.id.layout_movie_wrapper); // creating the progress dialog progressDialog = new ProgressDialog(MainActivity. this ); progressDialog.setMessage( "Please wait.." ); progressDialog.setCancelable( false ); progressDialog.setCanceledOnTouchOutside( false ); // set up the onClickListeners cancel.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { // create an intent to retrieve the video // file from the device storage Intent intent = new Intent( Intent.ACTION_PICK, android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI); intent.setType( "video/*" ); startActivityForResult(intent, 123 ); } }); slow.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { // check if the user has selected any video or not // In case a user hasn't selected any video and press the button, // we will show an warning, stating "Please upload the video" if (video_url != null ) { // a try-catch block to handle all necessary exceptions // like File not found, IOException try { slowmotion(rangeSeekBar.getSelectedMinValue().intValue() * 1000 , rangeSeekBar.getSelectedMaxValue().intValue() * 1000 ); } catch (Exception e) { Toast.makeText(MainActivity. this , e.toString(), Toast.LENGTH_SHORT).show(); e.printStackTrace(); } } else Toast.makeText(MainActivity. this , "Please upload video" , Toast.LENGTH_SHORT).show(); } }); fast.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { if (video_url != null ) { try { fastforward(rangeSeekBar.getSelectedMinValue().intValue() * 1000 , rangeSeekBar.getSelectedMaxValue().intValue() * 1000 ); } catch (Exception e) { e.printStackTrace(); Toast.makeText(MainActivity. this , e.toString(), Toast.LENGTH_SHORT).show(); } } else Toast.makeText(MainActivity. this , "Please upload video" , Toast.LENGTH_SHORT).show(); } }); reverse.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { if (video_url != null ) { try { reverse(rangeSeekBar.getSelectedMinValue().intValue() * 1000 , rangeSeekBar.getSelectedMaxValue().intValue() * 1000 ); } catch (Exception e) { e.printStackTrace(); Toast.makeText(MainActivity. this , e.toString(), Toast.LENGTH_SHORT).show(); } } else Toast.makeText(MainActivity. this , "Please upload video" , Toast.LENGTH_SHORT).show(); } }); // set up the VideoView. // We will be using VideoView to view our video videoView.setOnPreparedListener( new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { // get the duration of the video int duration = mp.getDuration() / 1000 ; // initially set the left TextView to "00:00:00" tvLeft.setText( "00:00:00" ); // initially set the right Text-View to the video length // the getTime() method returns a formatted string in hh:mm:ss tvRight.setText(getTime(mp.getDuration() / 1000 )); // this will run he video in loop // i.e. the video won't stop // when it reaches its duration mp.setLooping( true ); // set up the initial values of rangeSeekbar rangeSeekBar.setRangeValues( 0 , duration); rangeSeekBar.setSelectedMinValue( 0 ); rangeSeekBar.setSelectedMaxValue(duration); rangeSeekBar.setEnabled( true ); rangeSeekBar.setOnRangeSeekBarChangeListener( new RangeSeekBar.OnRangeSeekBarChangeListener() { @Override public void onRangeSeekBarValuesChanged(RangeSeekBar bar, Object minValue, Object maxValue) { // we seek through the video when the user // drags and adjusts the seekbar videoView.seekTo(( int ) minValue * 1000 ); // changing the left and right TextView according to // the minValue and maxValue tvLeft.setText(getTime(( int ) bar.getSelectedMinValue())); tvRight.setText(getTime(( int ) bar.getSelectedMaxValue())); } }); // this method changes the right TextView every 1 second // as the video is being played // It works same as a time counter we see in any Video Player final Handler handler = new Handler(); handler.postDelayed(r = new Runnable() { @Override public void run() { if (videoView.getCurrentPosition() >= rangeSeekBar.getSelectedMaxValue().intValue() * 1000 ) videoView.seekTo(rangeSeekBar.getSelectedMinValue().intValue() * 1000 ); handler.postDelayed(r, 1000 ); } }, 1000 ); } }); } // Method for creating fast motion video private void fastforward( int startMs, int endMs) throws Exception { // startMs is the starting time, from where we have to apply the effect. // endMs is the ending time, till where we have to apply effect. // For example, we have a video of 5min and we only want to fast forward a part of video // say, from 1:00 min to 2:00min, then our startMs will be 1000ms and endMs will be 2000ms. // create a progress dialog and show it until this method executes. progressDialog.show(); // creating a new file in storage final String filePath; String filePrefix = "fastforward" ; String fileExtn = ".mp4" ; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // With introduction of scoped storage in Android Q the primitive method gives error // So, it is recommended to use the below method to create a video file in storage. ContentValues valuesvideos = new ContentValues(); valuesvideos.put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/" + "Folder" ); valuesvideos.put(MediaStore.Video.Media.TITLE, filePrefix + System.currentTimeMillis()); valuesvideos.put(MediaStore.Video.Media.DISPLAY_NAME, filePrefix + System.currentTimeMillis() + fileExtn); valuesvideos.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4" ); valuesvideos.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000 ); valuesvideos.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis()); Uri uri = getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, valuesvideos); // get the path of the video file created in the storage. File file = FileUtils.getFileFromUri( this , uri); filePath = file.getAbsolutePath(); } else { // This else statement will work for devices with Android version lower than 10 // Here, "app_folder" is the path to your app's root directory in device storage File dest = new File( new File(app_folder), filePrefix + fileExtn); int fileNo = 0 ; // check if the file name previously exist. Since we don't want // to overwrite the video files while (dest.exists()) { fileNo++; dest = new File( new File(app_folder), filePrefix + fileNo + fileExtn); } // Get the filePath once the file is successfully created. filePath = dest.getAbsolutePath(); } String exe; // the "exe" string contains the command to process video.The details of command are discussed later in this post. // "video_url" is the url of video which you want to edit. You can get this url from intent by selecting any video from gallery. exe = "-y -i " + video_url + " -filter_complex [0:v]trim=0:" + startMs / 1000 + ",setpts=PTS-STARTPTS[v1];[0:v]trim=" + startMs / 1000 + ":" + endMs / 1000 + ",setpts=0.5*(PTS-STARTPTS)[v2];[0:v]trim=" + (endMs / 1000 ) + ",setpts=PTS-STARTPTS[v3];[0:a]atrim=0:" + (startMs / 1000 ) + ",asetpts=PTS-STARTPTS[a1];[0:a]atrim=" + (startMs / 1000 ) + ":" + (endMs / 1000 ) + ",asetpts=PTS-STARTPTS,atempo=2[a2];[0:a]atrim=" + (endMs / 1000 ) + ",asetpts=PTS-STARTPTS[a3];[v1][a1][v2][a2][v3][a3]concat=n=3:v=1:a=1 " + "-b:v 2097k -vcodec mpeg4 -crf 0 -preset superfast " + filePath; // Here, we have used he Async task to execute our query because // if we use the regular method the progress dialog // won't be visible. This happens because the regular method and // progress dialog uses the same thread to execute // and as a result only one is a allowed to work at a time. // By using we Async task we create a different thread which resolves the issue. long executionId = FFmpeg.executeAsync(exe, new ExecuteCallback() { @Override public void apply( final long executionId, final int returnCode) { if (returnCode == RETURN_CODE_SUCCESS) { // after successful execution of ffmpeg command, // again set up the video Uri in VideoView videoView.setVideoURI(Uri.parse(filePath)); // change the video_url to filePath, so that we could // do more manipulations in the // resultant video. By this we can apply as many effects // as we want in a single video. // Actually there are multiple videos being formed in // storage but while using app it // feels like we are doing manipulations in only one video video_url = filePath; // play the result video in VideoView videoView.start(); // remove the progress dialog progressDialog.dismiss(); } else if (returnCode == RETURN_CODE_CANCEL) { Log.i(Config.TAG, "Async command execution cancelled by user." ); } else { Log.i(Config.TAG, String.format( "Async command execution failed with returnCode=%d." , returnCode)); } } }); } // Method for creating slow motion video for specific part of the video // The below code is same as above only the command in string "exe" is changed private void slowmotion( int startMs, int endMs) throws Exception { progressDialog.show(); final String filePath; String filePrefix = "slowmotion" ; String fileExtn = ".mp4" ; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ContentValues valuesvideos = new ContentValues(); valuesvideos.put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/" + "Folder" ); valuesvideos.put(MediaStore.Video.Media.TITLE, filePrefix + System.currentTimeMillis()); valuesvideos.put(MediaStore.Video.Media.DISPLAY_NAME, filePrefix + System.currentTimeMillis() + fileExtn); valuesvideos.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4" ); valuesvideos.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000 ); valuesvideos.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis()); Uri uri = getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, valuesvideos); File file = FileUtils.getFileFromUri( this , uri); filePath = file.getAbsolutePath(); } else { File dest = new File( new File(app_folder), filePrefix + fileExtn); int fileNo = 0 ; while (dest.exists()) { fileNo++; dest = new File( new File(app_folder), filePrefix + fileNo + fileExtn); } filePath = dest.getAbsolutePath(); } String exe; exe = "-y -i " + video_url + " -filter_complex [0:v]trim=0:" + startMs / 1000 + ",setpts=PTS-STARTPTS[v1];[0:v]trim=" + startMs / 1000 + ":" + endMs / 1000 + ",setpts=2*(PTS-STARTPTS)[v2];[0:v]trim=" + (endMs / 1000 ) + ",setpts=PTS-STARTPTS[v3];[0:a]atrim=0:" + (startMs / 1000 ) + ",asetpts=PTS-STARTPTS[a1];[0:a]atrim=" + (startMs / 1000 ) + ":" + (endMs / 1000 ) + ",asetpts=PTS-STARTPTS,atempo=0.5[a2];[0:a]atrim=" + (endMs / 1000 ) + ",asetpts=PTS-STARTPTS[a3];[v1][a1][v2][a2][v3][a3]concat=n=3:v=1:a=1 " + "-b:v 2097k -vcodec mpeg4 -crf 0 -preset superfast " + filePath; long executionId = FFmpeg.executeAsync(exe, new ExecuteCallback() { @Override public void apply( final long executionId, final int returnCode) { if (returnCode == RETURN_CODE_SUCCESS) { videoView.setVideoURI(Uri.parse(filePath)); video_url = filePath; videoView.start(); progressDialog.dismiss(); } else if (returnCode == RETURN_CODE_CANCEL) { Log.i(Config.TAG, "Async command execution cancelled by user." ); } else { Log.i(Config.TAG, String.format( "Async command execution failed with returnCode=%d." , returnCode)); } } }); } // Method for reversing the video // The below code is same as above only the command is changed. private void reverse( int startMs, int endMs) throws Exception { progressDialog.show(); String filePrefix = "reverse" ; String fileExtn = ".mp4" ; final String filePath; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ContentValues valuesvideos = new ContentValues(); valuesvideos.put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/" + "Folder" ); valuesvideos.put(MediaStore.Video.Media.TITLE, filePrefix + System.currentTimeMillis()); valuesvideos.put(MediaStore.Video.Media.DISPLAY_NAME, filePrefix + System.currentTimeMillis() + fileExtn); valuesvideos.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4" ); valuesvideos.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000 ); valuesvideos.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis()); Uri uri = getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, valuesvideos); File file = FileUtils.getFileFromUri( this , uri); filePath = file.getAbsolutePath(); } else { filePrefix = "reverse" ; fileExtn = ".mp4" ; File dest = new File( new File(app_folder), filePrefix + fileExtn); int fileNo = 0 ; while (dest.exists()) { fileNo++; dest = new File( new File(app_folder), filePrefix + fileNo + fileExtn); } filePath = dest.getAbsolutePath(); } long executionId = FFmpeg.executeAsync( "-y -i " + video_url + " -filter_complex [0:v]trim=0:" + endMs / 1000 + ",setpts=PTS-STARTPTS[v1];[0:v]trim=" + startMs / 1000 + ":" + endMs / 1000 + ",reverse,setpts=PTS-STARTPTS[v2];[0:v]trim=" + (startMs / 1000 ) + ",setpts=PTS-STARTPTS[v3];[v1][v2][v3]concat=n=3:v=1 " + "-b:v 2097k -vcodec mpeg4 -crf 0 -preset superfast " + filePath, new ExecuteCallback() { @Override public void apply( final long executionId, final int returnCode) { if (returnCode == RETURN_CODE_SUCCESS) { videoView.setVideoURI(Uri.parse(filePath)); video_url = filePath; videoView.start(); progressDialog.dismiss(); } else if (returnCode == RETURN_CODE_CANCEL) { Log.i(Config.TAG, "Async command execution cancelled by user." ); } else { Log.i(Config.TAG, String.format( "Async command execution failed with returnCode=%d." , returnCode)); } } }); } // Overriding the method onActivityResult() // to get the video Uri form intent. @Override protected void onActivityResult( int requestCode, int resultCode, @Nullable Intent data) { super .onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == 123 ) { if (data != null ) { // get the video Uri Uri uri = data.getData(); try { // get the file from the Uri using getFileFromUri() method present // in FileUils.java File video_file = FileUtils.getFileFromUri( this , uri); // now set the video uri in the VideoView videoView.setVideoURI(uri); // after successful retrieval of the video and properly // setting up the retried video uri in // VideoView, Start the VideoView to play that video videoView.start(); // get the absolute path of the video file. We will require // this as an input argument in // the ffmpeg command. video_url = video_file.getAbsolutePath(); } catch (Exception e) { Toast.makeText( this , "Error" , Toast.LENGTH_SHORT).show(); e.printStackTrace(); } } } } } // This method returns the seconds in hh:mm:ss time format private String getTime( int seconds) { int hr = seconds / 3600 ; int rem = seconds % 3600 ; int mn = rem / 60 ; int sec = rem % 60 ; return String.format( "%02d" , hr) + ":" + String.format( "%02d" , mn) + ":" + String.format( "%02d" , sec); } } |
Step 6: Creating a new Java Class FileUtils.java
Refer to How to Create Classes in Android Studio to create a new java class in Android Studio. This is a Utility file that will help in retrieving the File from a Uri. Below is the code for the FileUtils.java file. Comments are added inside the code to understand the code in more detail.
Java
import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.DocumentsContract; import android.provider.MediaStore; import java.io.File; public class FileUtils { // Get a file from a Uri. // Framework Documents, as well as the _data field for the MediaStore and // other file-based ContentProviders. // @param context The context. // @param uri The Uri to query public static File getFileFromUri( final Context context, final Uri uri) throws Exception { String path = null ; // DocumentProvider if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (DocumentsContract.isDocumentUri(context, uri)) { // TODO: 2015. 11. 17. KITKAT // ExternalStorageProvider if (isExternalStorageDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split( ":" ); final String type = split[ 0 ]; if ( "primary" .equalsIgnoreCase(type)) { path = Environment.getExternalStorageDirectory() + "/" + split[ 1 ]; } // TODO handle non-primary volumes } else if (isDownloadsDocument(uri)) { // DownloadsProvider final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId(Uri.parse( "content://downloads/public_downloads" ), Long.valueOf(id)); path = getDataColumn(context, contentUri, null , null ); } else if (isMediaDocument(uri)) { // MediaProvider final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split( ":" ); final String type = split[ 0 ]; Uri contentUri = null ; if ( "image" .equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ( "video" .equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ( "audio" .equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } final String selection = "_id=?" ; final String[] selectionArgs = new String[]{ split[ 1 ] }; path = getDataColumn(context, contentUri, selection, selectionArgs); } // MediaStore (and general) } else if ( "content" .equalsIgnoreCase(uri.getScheme())) { path = getDataColumn(context, uri, null , null ); } // File else if ( "file" .equalsIgnoreCase(uri.getScheme())) { path = uri.getPath(); } return new File(path); } else { Cursor cursor = context.getContentResolver().query(uri, null , null , null , null ); return new File(cursor.getString(cursor.getColumnIndex( "_data" ))); } } // Get the value of the data column for this Uri. This is useful for // MediaStore Uris, and other file-based ContentProviders. // @param context The context. // @param uri The Uri to query. // @param selection (Optional) Filter used in the query. // @param selectionArgs (Optional) Selection arguments used in the query. // @return The value of the _data column, which is typically a file path. public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null ; final String column = MediaStore.Images.Media.DATA; final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null ); if (cursor != null && cursor.moveToFirst()) { final int column_index = cursor.getColumnIndexOrThrow(column); return cursor.getString(column_index); } } finally { if (cursor != null ) cursor.close(); } return null ; } // @param uri The Uri to check. // @return Whether the Uri authority is ExternalStorageProvide public static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents" .equals(uri.getAuthority()); } // @param uri The Uri to check. // @return Whether the Uri authority is DownloadsProvider. public static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents" .equals(uri.getAuthority()); } // @param uri The Uri to check. // @return Whether the Uri authority is MediaProvider. public static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents" .equals(uri.getAuthority()); } } |
Output:
Github Project Link: https://github.com/raghavtilak/VideoEditor
Few FFmpeg commands that you can play with
- concat video of different framerates in .mkv format:
- -i input1.mp4 -i input2.mp4 -filter_complex [0:v:0][0:a:0][1:v:0][1:a:0]concat=n=2:v=1:a=1[outv][outa] -map [outv] -map [outa] output.mkv
- concat video with no sound/audio:
- -y -i input.mp4 -filter_complex [0:v]trim=0:0,setpts=PTS-STARTPTS[v1];[0:v]trim=0:5,setpts=0.5*(PTS-STARTPTS)[v2];[0:v]trim=5,setpts=PTS-STARTPTS[v3];[v1][v2][v3]concat=n=3:v=1:a=0 -b:v 2097k -vcodec mpeg4 -crf 0 -preset superfast output.mp4
- Textoverlay:
- -y -i input.mp4 -vf drawtext=”fontsize=30:fontfile=cute.ttf:text=’GFG'”:x=w-tw-10:y=h-th-10 -c:v libx264 -preset ultrafast outputmp4
- gif/png/jpeg overlay
- -i input.mp4 -i inputimage.png -filter_complex [1:v]scale=320:394[ovr1],[0:v][ovr1]overlay=0:0:enable=’between(t,0,16)’ -c:a copy output.mp4, (or)
- -i input.mp4 -i inputimage.png -filter_complex overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2:enable=’between(t,0,7)’ -c:a copy output.mp4
- Add subtitles to a video file
- -i input.mp4 -i subtitle.srt -map 0 -map 1 -c copy -c:v libx264 -crf 23 -preset superfast output.mp4
- Converting video files to audio files
- -i input.mp4 -vn output.mp3
- Cropping videos
- -i input.mp4 -filter:v “crop=w:h:x:y” output.mp4
- w – width of the rectangle which we are intended to crop from the source video.
- h – the height of that rectangle.
- x – the x coordinate of that rectangle.
- y – the y coordinate of the rectangle.
- Adding a poster image to audio files
- -loop 1 -i inputimage.jpg -i inputaudio.mp3 -c:v libx264 -c:a aac -strict experimental -b:a 192k -shortest output.mp4
Pros of using FFmpeg
- It is also highly portable.
- It is profoundly valuable for the transcoding of all kinds of multimedia files into a single common format.
- You don’t need heavy Third-party VideoEditors like Adobe Premiere Pro, Filmora for small editing tasks.
Cons of using FFmpeg
- It’s difficult for beginners to use and implement.
- It takes some time to process. We don’t get results in a second or two.
- The official documentation is quite confusing and it’s not beginner-friendly.
- APK size becomes very large. The FFmpeg libraries alone will use 30-70MB depending upon the libraries you are including.
Alternatives of FFmpeg
- MediaCodec Android
- LiTr
- Gstreamer
- MP4Parser
- Intel INDE Media for Mobile
Notes:
1. If you set -preset to a higher value say, ultrafast then the video processing will be fast but the quality of the video will be compromised. The lower the -preset higher the quality of the video.
2. You can change the -crf value to change the quality of the output video. Lower the crf value higher the quality of the video.
3. If you use -y in starting of command then this means that if a file is present with the same name as that of the output file name that FFmpeg will overwrite the existing file.
4. In the case of video, to slow down the video set -PTS value larger than 1. The larger the value slower the video, Lower the value Faster the video. But in the case of Audio this is just the opposite, i.e. Larger the value faster the Audio, the Lower the value slower the audio.
5. The atempo(audio) filter is limited to using values between 0.5 and 2.0 (so it can slow it down to no less than half the original speed, and speed up to no more than double the input)
6. FFmpeg takes too much time working with audio. If the video file doesn’t contain the audio we need not to command FFmpeg to work with audio, and hence this will reduce the workload and we will get the processed video fast/in less time.
Please Login to comment...