Android 獲取 FFmpeg 執(zhí)行進(jìn)度
在以命令方式調(diào)用 FFmpeg 的時(shí)候,可能會(huì)執(zhí)行一些比較耗時(shí)的任務(wù),這時(shí)如果沒有進(jìn)度展示,用戶可能會(huì)以為程序崩潰了,體驗(yàn)十分不好。
能不能在以命令方式調(diào)用 FFmpeg 時(shí)實(shí)時(shí)獲取執(zhí)行進(jìn)度呢?
谷歌關(guān)鍵詞 “Android FFmpeg 命令” 可以得到很多教程,但加上關(guān)鍵詞 "進(jìn)度"就沒有相關(guān)文章了,看來以命令方式調(diào)用 FFmpeg 實(shí)時(shí)獲取執(zhí)行進(jìn)度這個(gè)需求沒有前人的肩膀可站,要開動(dòng)自己的小腦筋了。
首先來分析一下,以命令方式調(diào)用就是把一條命令交給 FFmpeg 執(zhí)行,具體就是 ffmpeg.c 的 main 函數(shù),待 main 函數(shù)執(zhí)行完畢才會(huì)返回,執(zhí)行過程相當(dāng)于一個(gè)黑盒,執(zhí)行進(jìn)度顯然是無法獲取的。
網(wǎng)上也沒有相關(guān)文章,難道只有以函數(shù)方式調(diào)用 FFmpeg 才能獲取到執(zhí)行進(jìn)度嗎?當(dāng)我快要下這樣的定論時(shí),看到了 FFmpeg 的 log 信息:

這是在執(zhí)行混合音頻命令時(shí) FFmpeg 的日志輸出,其中的 time 信息表示當(dāng)前已合成的音頻時(shí)長,這不就是進(jìn)度信息嗎!
下面就針對(duì)混合音頻命令獲取實(shí)時(shí)執(zhí)行進(jìn)度.要做的就是提取日志中的進(jìn)度信息,傳遞給 Android 層,首先回顧一下這些日志信息是怎樣輸出到 logcat 的,在Android 集成 FFmpeg(二) 以命令方式調(diào)用中有詳細(xì)說明,這里只關(guān)注關(guān)鍵方法 log_callback_null ,位于 ffmpeg.c 中:
static void log_callback_null(void *ptr, int level, const char *fmt, va_list vl)
{
static int print_prefix = 1;
static int count;
static char prev[1024];
char line[1024];
static int is_atty;
av_log_format_line(ptr, level, fmt, vl, line, sizeof(line), &print_prefix);
strcpy(prev, line);
if (level <= AV_LOG_WARNING){
XLOGE("%s", line);
}else{
XLOGD("%s", line);
}
}
日志信息都是通過第 13 行的 XLOGD 方法輸入到 logcat 中的,我們需要的進(jìn)度信息就在 line 字符串中,那只要在此處把進(jìn)度提取出來傳遞給 Android 層就行了,在 XLOGD 方法下添加一個(gè)傳遞方法:
XLOGD("%s", line);
callJavaMethod(line);//傳遞進(jìn)度信息
callJavaMethod 方法直接在 com_jni_FFmpegJni.c 接口文件中定義即可,在實(shí)現(xiàn)此方法前先明確要做什么.首先要對(duì)日志信息進(jìn)行處理,把進(jìn)度提取出來,日志信息形如:
frame= 1 fps=0.0 q=0.0 size= 0kB time=00:01:02.71 bitrate= 0.0kbits/s speed=2.88x
void callJavaMethod(char *ret) {
int result = 0;
char timeStr[10] = "time=";
char *q = strstr(ret, timeStr);
if(q != NULL){ //日志信息中若包含"time="字符串
char str[14] = {0};
strncpy(str, q, 13);
int h =(str[5]-'0')*10+(str[6]-'0');
int m =(str[8]-'0')*10+(str[9]-'0');
int s =(str[11]-'0')*10+(str[12]-'0');
result = s+m*60+h*60*60;
}else{
return;
}
//已執(zhí)行時(shí)長 result
}
public static void onProgress(int second) {
}
//獲取java方法
jmethodID methodID = (*m_env)->GetStaticMethodID(m_env, m_clazz, "onProgress", "(I)V");
//調(diào)用該方法
(*m_env)->CallStaticVoidMethod(m_env, m_clazz, methodID,result);
static jclass m_clazz = NULL;//當(dāng)前類(面向java)
static JNIEnv *m_env = NULL;
JNIEXPORT jint JNICALL Java_com_jni_FFmpegJni_run(JNIEnv *env, jclass clazz, jobjectArray commands) {
//獲取java虛擬機(jī),在jni的c線程中不允許使用共用的env環(huán)境變量 但JavaVM在整個(gè)jvm中是共用的 可通過保存JavaVM指針,到時(shí)候再通過JavaVM指針取出JNIEnv *env
(*env)->GetJavaVM(env, &jvm);
//獲取調(diào)用此方法的java類,ICS之前(你可把NDK sdk版本改成低于11) 可以寫m_clazz = clazz直接賦值, 然而ICS(sdk11) 后便改變了這一機(jī)制,在線程中回調(diào)java時(shí) 不能直接共用變量 必須使用NewGlobalRef創(chuàng)建全局對(duì)象
m_clazz = (*env)->NewGlobalRef(env, clazz);
m_env = env;
//以命令方式調(diào)用 FFmpeg
...
}
這樣就可以實(shí)現(xiàn) c 語言中調(diào)用 java 方法了,進(jìn)度以形參傳遞到 Java 層,修改 onProgress 方法測試一下:
public static void onProgress(int second) {
Log.d("AAA", "已執(zhí)行時(shí)長:" + second);
}

合成音頻命令的關(guān)鍵詞為"amix",F(xiàn)Fmpeg 開始執(zhí)行這個(gè)命令時(shí),會(huì)輸出包含 “amix” 字符串的日志信息,那我們就可以再次使用 strstr 方法過濾日志信息,com_jni_FFmpegJni.c 完整代碼如下:
#include "android_log.h"
#include "com_jni_FFmpegJni.h"
#include "ffmpeg.h"
#include <string.h>
static JavaVM *jvm = NULL;//java虛擬機(jī)
static jclass m_clazz = NULL;//當(dāng)前類(面向java)
static JNIEnv *m_env = NULL;
static char amixStr[10] = "amix";
static char timeStr[10] = "time=";
static char amixing = 0; //0:沒遇到 1:遇到
/**
* 回調(diào)執(zhí)行Java方法
*/
void callJavaMethod(char *ret) {
char *p = strstr(ret, amixStr);
if(p != NULL){
//LOGE("遇到amix");
amixing = 1;
}
int ss=0;
if(amixing == 1){
char *q = strstr(ret, timeStr);
if(q != NULL){
//LOGE("遇到time=");
char str[14] = {0};
strncpy(str, q, 13);
int h =(str[5]-'0')*10+(str[6]-'0');
int m =(str[8]-'0')*10+(str[9]-'0');
int s =(str[11]-'0')*10+(str[12]-'0');
ss = s+m*60+h*60*60;
}else{
return;
}
}else{
return;
}
if (m_clazz == NULL) {
LOGE("---------------clazz isNULL---------------");
return;
}
//獲取方法ID (I)V指的是方法簽名 通過javap -s -public FFmpegCmd 命令生成
jmethodID methodID = (*m_env)->GetStaticMethodID(m_env, m_clazz, "onProgress", "(I)V");
if (methodID == NULL) {
LOGE("---------------methodID isNULL---------------");
return;
}
//調(diào)用該java方法
(*m_env)->CallStaticVoidMethod(m_env, m_clazz, methodID,ss);
}
JNIEXPORT jint JNICALL Java_com_jni_FFmpegJni_run(JNIEnv *env, jclass clazz, jobjectArray commands) {
//獲取java虛擬機(jī),在jni的c線程中不允許使用共用的env環(huán)境變量 但JavaVM在整個(gè)jvm中是共用的 可通過保存JavaVM指針,到時(shí)候再通過JavaVM指針取出JNIEnv *env
(*env)->GetJavaVM(env, &jvm);
//獲取調(diào)用此方法的java類,ICS之前(你可把NDK sdk版本改成低于11) 可以寫m_clazz = clazz直接賦值, 然而ICS(sdk11) 后便改變了這一機(jī)制,在線程中回調(diào)java時(shí) 不能直接共用變量 必須使用NewGlobalRef創(chuàng)建全局對(duì)象
m_clazz = (*env)->NewGlobalRef(env, clazz);
m_env = env;
int argc = (*env)->GetArrayLength(env, commands);
char *argv[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);
}
amixing = 0;
int ret = main(argc, argv);
amixing = 0;
return ret;
}
接下來再完善一下 FFmpegJni.java,針對(duì)本案例,我把合成音頻命令和進(jìn)度回調(diào)進(jìn)行了簡單封裝,完整代碼如下:
public class FFmpegJni {
private static OnAmixProgressListener mOnAmixProgressListener;
public static void onProgress(int second) {
if (mOnAmixProgressListener != null && second >= 0) {
mOnAmixProgressListener.onProgress(second);
}
}
public interface OnAmixProgressListener {
void onProgress(int second);
}
public static void mixAudio(String srcAudioPath, List<String> audioPathList, String outputPath, OnAmixProgressListener onAmixProgressListener) {
mOnAmixProgressListener = onAmixProgressListener;
_mixAudio(srcAudioPath, audioPathList, outputPath);
}
private static void _mixAudio(String srcAudioPath, List<String> audioPathList, String outputPath) {
ArrayList<String> commandList = new ArrayList<>();
commandList.add("ffmpeg");
commandList.add("-i");
commandList.add(srcAudioPath);
for (String audioPath : audioPathList) {
commandList.add("-i");
commandList.add(audioPath);
}
commandList.add("-filter_complex");
commandList.add("amix=inputs=" + (audioPathList.size()+1) + ":duration=first:dropout_transition=1");
commandList.add("-f");
commandList.add("mp3");
commandList.add("-ac");//聲道數(shù)
commandList.add("1");
commandList.add("-ar"); //采樣率
commandList.add("24k");
commandList.add("-ab");//比特率
commandList.add("32k");
commandList.add("-y");
commandList.add(outputPath);
String[] commands = new String[commandList.size()];
commandList.toArray(commands);
run(commands);
}
static {
System.loadLibrary("avutil-55");
System.loadLibrary("avcodec-57");
System.loadLibrary("avformat-57");
System.loadLibrary("avdevice-57");
System.loadLibrary("swresample-2");
System.loadLibrary("swscale-4");
System.loadLibrary("postproc-54");
System.loadLibrary("avfilter-6");
System.loadLibrary("ffmpeg");
}
public static native int run(String[] commands);
}
有了當(dāng)前已合成時(shí)長,再結(jié)合總時(shí)長,就能得到命令執(zhí)行的百分比進(jìn)度了,
MainActivity.java 如下:
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.textView);
mButton = (Button) findViewById(R.id.button);
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
String dir = Environment.getExternalStorageDirectory().getPath() + "/ffmpegTest/";
String srcAudio = dir + "paomo.mp3";
String audio1 = dir + "tonghuazhen.mp3";
String outputAudio = dir + "outputAudio.mp3";
List<String> audioPaths = new ArrayList<>();
audioPaths.add(audio1);
final int duration = getDuration(srcAudio);
FFmpegJni.mixAudio(srcAudio, audioPaths, outputAudio, new FFmpegJni.OnAmixProgressListener() {
@Override
public void onProgress(int second) {
final String percent = format((second / (float) duration) * 100);
Log.d("FFMPEG", "second=" + second + " duration=" + duration +
" percent=" + percent);
mTextView.post(new Runnable() {
@Override
public void run() {
mTextView.setText("已執(zhí)行:" + percent);
}
});
}
});
}
}).start();
}
});
}
public int getDuration(String audioPath) {
MediaPlayer player = new MediaPlayer();
try {
player.setDataSource(audioPath);
player.prepare();
} catch (IOException e) {
e.printStackTrace();
}
int duration = (int) Math.round(player.getDuration() / 1000.0);
player.release();
return duration;
}
public static String format(float value) {
return String.format("%.2f", value) + "%";
}
}
進(jìn)度效果如下:

最后貼一個(gè)音頻合成效果,泡沫&童話鎮(zhèn)混合后的效果,感受一下 amix 命令的魅(噪)力(音)吧。
版權(quán)聲明:本文為CSDN博主「王英豪」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/yhaolpz/article/details/78350435
項(xiàng)目地址:https://github.com/yhaolpz/FFmpegCmd
-- END --
推薦:
