我们这篇主要了解使用FFmpeg命令行如何配置。在编译FFmpeg的时候,使用了参数-disable-ffmpeg,这样不会生成FFmpeg工具,如果生成了在Android应用也用不了,但是我们可以通过jni对代码做一些修改,间接的使用命令行工具。这个工具真的非常强大,例如
本示例接着上一篇的应用下面,所以不用引入libffmpeg.so和前面一些ndk开发的配置,具体的环境和配置信息见Android音视频-FFmpeg编译为单个so与测试调用,本篇的配置也是基于Cmake来进行配置的,感觉它比ndk-build好用的多。
当我们要直接进行视频的各种转换的时候,又不想下载别的软件,可以选择在电脑里面安装一个ffmpeg工具,安装步骤很简单:
brew install ffmpeg
/usr/local/Cellar/ffmpeg/3.4.2
ffmpeg -i input.mp4 output.avi
注意这个input.mp4文件放到bin目录下面,否则找不到我们以前已经把FFmpeg的相关头文件和libffmpeg.so引入了。下面只要引入如下文件:
下面把根目录ffmpeg-3.3.6文件下面的ffmpeg.h,cmdutils.h,cmdutils_common_opts.h,config.h,ffmpeg.c, ffmpeg_opt.c, ffmpeg_filter.c,cmdutils.c, 文件引入进来,引入后cpp文件夹项目结构如下:
找到cmdutils.c中的exit_program函数,注释到 exit(ret), 添加 return ret;
并修改函数的返回类型为int, 找到cmdutils.h中exit_program的申明,也把返回类型修改为int。
看的一篇文章说到这里就可以但是,但是当实际运行多次执行指令的时候有问题,因为FFmpeg每次执行完会调用ffmpeg_cleanup函数清理内存,并且调用exit(0)结束当前进程,但是我们把exit方法删掉了,怕下次执行没有清理掉有问题,于是简单的把一些变量置空:
把如下代码加到修改的run方法的return之前:
nb_filtergraphs = 0;
progress_avio = NULL;
input_streams = NULL;
nb_input_streams = 0;
input_files = NULL;
nb_input_files = 0;
output_streams = NULL;
nb_output_streams = 0;
output_files = NULL;
nb_output_files = 0;
这个参考自这里
我没有去试会不会出现这个问题,这里留个笔记,怕以后遇到。
这里我们还是和以前一样,通过Java应用层声明native方法并且实现c文件以及它们之间的调用。
声明一个命令行工具类FFmpegKit.java,它调用底层的实现c代码
package com.lyman.ffmpeg_cmake_single.utils;
import java.util.ArrayList;
/**
* Author: lyman
* Email: [email protected]
* Date: 2018/3/9
* Description:
*/
public class FFmpegKit {
private ArrayList commands;
static {
System.loadLibrary("ffmpeg");
System.loadLibrary("ffmpeginvoke");
}
public FFmpegKit() {
this.commands = new ArrayList();
this.commands.add("ffmpeg");
}
public native static int run(String[] commands);
}
在Activity里面调用工具类方法:
package com.lyman.ffmpeg_cmake_single;
import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import com.lyman.ffmpeg_cmake_single.utils.FFmpegKit;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class CommandLineActivity extends AppCompatActivity {
private static final String TAG = "CommandLineActivity";
private ProgressBar mProgressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_command_line);
mProgressBar = findViewById(R.id.progressBar2);
if (!getMoveFile("input_video.mp4").exists()
|| !getMoveFile("input_audio.acc").exists()) {
new Thread(new Runnable() {
@Override
public void run() {
copyFilesFromRaw(CommandLineActivity.this, R.raw.input_video,
"input_video.mp4",
getExternalFilesDir(Environment.DIRECTORY_MOVIES).getAbsolutePath());
copyFilesFromRaw(CommandLineActivity.this, R.raw.input_audio,
"input_audio.acc",
getExternalFilesDir(Environment.DIRECTORY_MOVIES).getAbsolutePath());
}
}).start();
}
}
public void onClickStartCommand(View view) {
mProgressBar.setVisibility(View.VISIBLE);
new Thread(new Runnable() {
@Override
public void run() {
String base = getMoveFile("input_video.mp4").getParent();
Log.e("PATH", base);
String[] commands = new String[9];
commands[0] = "ffmpeg";
commands[1] = "-i";
commands[2] = base + "/input_video.mp4";
commands[3] = "-i";
commands[4] = base + "/input_audio.acc";
commands[5] = "-strict";
commands[6] = "-2";
commands[7] = "-y";
commands[8] = base + "/merge.mp4";
int result = FFmpegKit.run(commands);
Log.e("RESULT", result + "**********************");
runOnUiThread(new Runnable() {
@Override
public void run() {
mProgressBar.setVisibility(View.GONE);
}
});
}
}).start();
}
private File getMoveFile(String fileName) {
File rootFile = getExternalFilesDir(Environment.DIRECTORY_MOVIES);
// Create the storage directory if it does not exist
if (!rootFile.exists()) {
if (!rootFile.mkdirs()) {
Log.d(TAG, "failed to create directory");
return null;
}
}
String path = rootFile.getAbsolutePath() + File.separator + fileName;
return new File(path);
}
private void copyFilesFromRaw(Context context, int id, String fileName, String storagePath) {
Log.e(TAG, "copyFilesFromRaw: " + fileName + "start");
InputStream inputStream = context.getResources().openRawResource(id);
File file = new File(storagePath);
if (!file.exists()) {//如果文件夹不存在,则创建新的文件夹
file.mkdirs();
}
readInputStream(storagePath + File.separator + fileName, inputStream);
}
/**
* 读取输入流中的数据写入输出流
*
* @param storagePath 目标文件路径
* @param inputStream 输入流
*/
private void readInputStream(String storagePath, InputStream inputStream) {
File file = new File(storagePath);
try {
if (!file.exists()) {
// 1.建立通道对象
FileOutputStream fos = new FileOutputStream(file);
// 2.定义存储空间
byte[] buffer = new byte[inputStream.available()];
// 3.开始读文件
int lenght = 0;
while ((lenght = inputStream.read(buffer)) != -1) {// 循环从输入流读取buffer字节
// 将Buffer中的数据写到outputStream对象中
fos.write(buffer, 0, lenght);
}
fos.flush();// 刷新缓冲区
// 4.关闭流
fos.close();
inputStream.close();
Log.e(TAG, "readInputStream: " + storagePath);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里我吧raw下面的两个文件拷贝到了手机存储目录上面,便于底层C代码找路径。
这里我们实现的是FFmpegKit.java里面的run方法,建立ffmpeginvoke.c文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
#include "ffmpeg.h"
#include
/* Header for class com_lyman_ffmpeg_cmake_single_utils_FFmpegKit */
#ifndef _Included_com_lyman_ffmpeg_cmake_single_utils_FFmpegKit
#define _Included_com_lyman_ffmpeg_cmake_single_utils_FFmpegKit
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_lyman_ffmpeg_cmake_single_utils_FFmpegKit
* Method: run
* Signature: ([Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_com_lyman_ffmpeg_1cmake_1single_utils_FFmpegKit_run
(JNIEnv *env, jclass obj, jobjectArray commands) {
int argc = (*env)->GetArrayLength(env, commands);
char *argv[argc];
__android_log_print(ANDROID_LOG_ERROR, "Kit", "argc %d\n", argc);
int i;
for (i = 0; i < argc; i++) {
jstring js = (jstring) (*env)->GetObjectArrayElement(env, commands, i);
argv[i] = (char *) (*env)->GetStringUTFChars(env, js, 0);
__android_log_print(ANDROID_LOG_ERROR, "Kit", "argv %s\n", argv[i]);
}
return run(argc, argv);
}
#ifdef __cplusplus
}
#endif
#endif
这里的配置真的非常关键
add_library( # Sets the name of the library.
ffmpeginvoke
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/include/cmdutils.c
src/main/cpp/include/ffmpeg.c
src/main/cpp/include/ffmpeg_filter.c
src/main/cpp/include/ffmpeg_opt.c
src/main/cpp/ffmpeginvoke.c)
target_link_libraries(
ffmpeginvoke
libffmpeg
${log-lib})
OK,我以为万事具备了,点击build等待libffmpeginvoke.so生成出来。一点击如下报错:
target_link_libraries(
ffmpeginvoke
libffmpeg
${log-lib})
的时候把libffmpeg没有链接进去,导致很多方法没有定义,几百个错误,找了整整一天发现问题。。。
开始测试功能是否可用:我上面的FFmpeg指令是合并音频和视频文件为一个mp4文件,我取的之前使用MediaMuxer合并的时候用的文件,视频15兆左右,音频5兆左右,执行以后速度非常的缓慢,运行了大概一两分钟完成了最后的合成文件,打开文件,可以播放。欣喜不已。看看手机文件目录:
参考链接:
编译Android下可执行命令的FFmpeg
项目完整代码:查看