FFmpeg, abreviatura de Fast-forward MPEG, es un marco multimedia gratuito y de código abierto, que puede decodificar, codificar, transcodificar, mux, demux, transmitir, filtrar y reproducir todo tipo de archivos multimedia que se han creado hasta la fecha. . También es compatible con algunos de los formatos más antiguos. FFmpeg compila y se ejecuta en varios sistemas operativos como Linux, Mac OS X, Microsoft Windows, BSD, Solaris, etc. entre una amplia gama de entornos de construcción, arquitecturas de máquinas y configuraciones. Los lenguajes de programación utilizados en FFmpeg son C y ensamblador. Podemos hacer muchas cosas divertidas usando Ffmpeg como,Compresión de video, Compresión de audio, Recortar video, Rotar video, Recortar video, Agregar filtros a videos, Invertir un video, Crear video en cámara rápida y lenta, Desvanecer gradualmente, Fusionar audio y video, Crear un video a partir de imágenes, Convertir video de un formato a otro, extraer imagen de video o sonido de video, superposición de gifs y muchos más. FFmpeg es parte del flujo de trabajo de cientos de otros proyectos de software relacionados con los medios y, a menudo, se usa detrás de escena. Además, es una parte interna del software como VLC media player, YouTube, Plex, iTunes, Shortcut, Blender, Kodi, HandBrake, maneja la reproducción de video y audio en Google Chrome y la versión Linux de Firefox. FFmpeg comprende una enorme configuración de bibliotecas y proyectos para manejar video, sonido y otros archivos y transmisiones multimedia.
Bibliotecas FFmpeg
- libavutil es una biblioteca de utilidades para ayudar a la programación multimedia versátil. Contiene funciones de strings portátiles, generadores de números arbitrarios, capacidades aritméticas adicionales, estructuras de datos, criptografía y utilidades multimedia básicas.
- libavcodec es una biblioteca que proporciona codificadores y decodificadores para códecs de video/audio, flujos de subtítulos y varios canales de flujo de bits.
- libavformat es una biblioteca que proporciona un marco de multiplexación y demultiplexación para códecs de video/audio, secuencias de subtítulos
- libavdevice es una biblioteca que contiene dispositivos de E/S para obtener y entregar numerosos sistemas de programación de E/S multimedia, incluidos Video4Linux, ALSA y VfW.
- La biblioteca libavfilter proporciona un marco de filtrado de medios que contiene varios filtros y sumideros.
- La biblioteca libswscale realiza tareas excepcionalmente mejoradas de escalado de imágenes y transformación de formato de píxeles.
- libswresample es una biblioteca que realiza cambios altamente optimizados pero con pérdidas en la tasa de audio, cambios en el diseño del canal, por ejemplo, de estéreo a mono, y operaciones de conversión de formato de muestra.
Android no tiene API eficientes y robustas para multimedia que puedan proporcionar funcionalidades como FFmpeg. La única API que tiene Android es la API de MediaCodec, pero es más rápida que FFmpeg porque usa el hardware del dispositivo para el procesamiento de video.
Cree una pequeña aplicación de edición de video en Android Studio usando FFmpeg
requisitos previos:
Antes de comenzar, debemos configurar un entorno para ejecutar nuestros comandos FFmpeg. Hay dos opciones para hacerlo:
- Construyendo nuestra propia biblioteca
- Mediante el uso de cualquier fuente compilada proporcionada por la comunidad. Hay muchas bibliotecas que se pueden usar para realizar operaciones FFmpeg en Android. Por ejemplo,
- EscrituraMentes
- Bravobit
- tanersener/móvil-ffmpeg
- yangjie10930/EpMedia: hay muchas funciones integradas presentes en esta biblioteca desde las que puede recortar, rotar, agregar un logotipo, agregar un filtro personalizado, fusionar diferentes videos.
Aunque se recomienda encarecidamente construir su biblioteca, ya que eso reducirá el tamaño de su apk, puede agregar una biblioteca de terceros y puede actualizar la biblioteca con el tiempo que desee. Pero, este proceso lleva mucho tiempo y requiere habilidades adicionales. Entonces, como principiante, puede usar algunas de las bibliotecas mencionadas anteriormente y, si tiene algún problema, puede plantearlo en su respectivo repositorio de GitHub. En el siguiente ejemplo, usaré tanersener/mobile-ffmpeg, ya que es compatible con el almacenamiento con alcance de Android 10 y también es la mejor biblioteca disponible en Internet para FFmpeg móvil. qué
Implementación paso a paso
Paso 1: Crear un nuevo proyecto
Para crear un nuevo proyecto en Android Studio, consulte Cómo crear/iniciar un nuevo proyecto en Android Studio . Tenga en cuenta que seleccione Java como lenguaje de programación.
Paso 2: agregar una dependencia al archivo build.gradle
Usaremos la biblioteca tanersener/mobile-ffmpeg para implementar las funcionalidades de FFmpeg en nuestra aplicación. Y también necesitaremos una barra de búsqueda de rango para seleccionar la duración particular del video. Así que agregue estas dependencias en el archivo build.gradle .
- implementación ‘com.arthenica:mobile-ffmpeg-full:4.4’
- implementación ‘org.florescu.android.rangeseekbar:rangeseekbar-library:0.3.0’
Paso 3: trabajar con el archivo colors.xml
A continuación se muestra el código para el archivo colors.xml .
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>
Paso 4: trabajar con el archivo activity_main.xml
Vaya al archivo activity_main.xml y consulte el siguiente código. A continuación se muestra el código para el archivo activity_main.xml .
XML
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" 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>
Paso 5: trabajar con el archivo MainActivity.java
Vaya al archivo MainActivity.java y consulte el siguiente código. A continuación se muestra el código del archivo MainActivity.java . Se agregan comentarios dentro del código para comprender el código con más detalle.
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); } }
Paso 6: crear una nueva clase Java FileUtils.java
Consulte Cómo crear clases en Android Studio para crear una nueva clase Java en Android Studio. Este es un archivo de utilidad que ayudará a recuperar el archivo de un Uri. A continuación se muestra el código para el archivo FileUtils.java . Se agregan comentarios dentro del código para comprender el código con más detalle.
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()); } }
Producción:
Enlace del proyecto Github: https://github.com/raghavtilak/VideoEditor
Pocos comandos FFmpeg con los que puedes jugar
- concate video de diferentes velocidades de cuadro en formato .mkv:
- -i entrada1.mp4 -i entrada2.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] salida.mkv
- concate video sin sonido/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 -salida ultrarrápida preestablecida.mp4
- Superposición de texto:
- -y -i input.mp4 -vf drawtext=”fontsize=30:fontfile=cute.ttf:text=’GFG’”:x=w-tw-10:y=h-th-10 -c:v libx264 – salida ultrarrápida preestablecidamp4
- superposición gif/png/jpeg
- -i input.mp4 -i inputimage.png -filter_complex [1:v]escala=320:394[ovr1],[0:v][ovr1]superposición=0:0:habilitar=’entre(t,0,16) )’ -c: una copia de salida.mp4, (o)
- -i input.mp4 -i inputimage.png -filter_complex overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2:enable=’entre(t,0,7)’ -c:una copia de salida.mp4
- Agregar subtítulos a un archivo de video
- -i input.mp4 -i subtitle.srt -map 0 -map 1 -c copy -c:v libx264 -crf 23 -preset superfast output.mp4
- Conversión de archivos de video a archivos de audio
- -i entrada.mp4 -vn salida.mp3
- Recortar vídeos
- -i entrada.mp4 -filtro:v “recortar=w:h:x:y” salida.mp4
- w : ancho del rectángulo que pretendemos recortar del video de origen.
- h – la altura de ese rectángulo.
- x – la coordenada x de ese rectángulo.
- y – la coordenada y del rectángulo.
- Adición de una imagen de póster a archivos de audio
- -bucle 1 -i inputimage.jpg -i inputaudio.mp3 -c:v libx264 -c:a aac -strict experimental -b:a 192k -shortest output.mp4
Ventajas de usar FFmpeg
- También es muy portátil.
- Es profundamente valioso para la transcodificación de todo tipo de archivos multimedia en un solo formato común.
- No necesita editores de video pesados de terceros como Adobe Premiere Pro, Filmora para pequeñas tareas de edición.
Contras de usar FFmpeg
- Es difícil de usar e implementar para los principiantes.
- Se necesita algo de tiempo para procesar. No obtenemos resultados en un segundo o dos.
- La documentación oficial es bastante confusa y no es apta para principiantes.
- El tamaño de APK se vuelve muy grande. Solo las bibliotecas FFmpeg usarán entre 30 y 70 MB, según las bibliotecas que estés incluyendo.
Alternativas de FFmpeg
- MediaCodec Android
- LiTr
- Gstreamer
- MP4Parser
- Medios Intel INDE para dispositivos móviles
Notas:
1. Si configura -preset en un valor más alto, por ejemplo, ultrarrápido, el procesamiento de video será rápido pero la calidad del video se verá comprometida. Cuanto menor sea el preajuste, mayor será la calidad del video.
2. Puede cambiar el valor -crf para cambiar la calidad del video de salida. Baje el valor crf mayor será la calidad del video.
3. Si usa -y al iniciar el comando, significa que si hay un archivo presente con el mismo nombre que el nombre del archivo de salida, FFmpeg sobrescribirá el archivo existente.
4. En el caso del video, para ralentizar el video, establezca un valor de PTS mayor que 1. Cuanto mayor sea el valor, más lento será el video, menor será el valor, más rápido será el video. Pero en el caso de Audio esto es todo lo contrario, es decir, cuanto mayor sea el valor, más rápido será el audio, cuanto menor sea el valor, más lento será el audio.
5. El filtro atempo (audio) se limita a usar valores entre 0,5 y 2,0 (por lo que puede reducir la velocidad a no menos de la mitad de la velocidad original y acelerar hasta no más del doble de la entrada)
6. FFmpeg tarda demasiado en trabajar con audio. Si el archivo de video no contiene el audio, no necesitamos ordenar a FFmpeg que funcione con audio y, por lo tanto, esto reducirá la carga de trabajo y obtendremos el video procesado más rápido/en menos tiempo.