在前一篇文章windows環(huán)境下編譯ffmpeg打包成單個so并使用Cmake集成到Android工程中 我們說到了將ffmpeg 編譯打包成單個so说贝,并使用cmake 集成到android工程中蜗元,現(xiàn)在我們來說說奋单,如何使用cmake 生成能夠使用jni 調用 ffmpeg命令工具。首先链瓦,你得按照前一篇文章所說的生成了libffmpeg.so包租冠,然后再執(zhí)行接下來的步驟搓谆。
1、將ffmpeg的頭文件復制到src/main/cpp/ffmpeg目錄绰寞,將ffmpeg中的cmdutils.c到逊、cmdutils.h、config.h滤钱、ffmpeg.h觉壶、ffmpeg.c、ffmpeg_filter.c件缸、ffmpeg_opt.c復制到src/main/cpp/ffmpeg目錄铜靶。
2、修改 ffmpeg.c 文件他炊,將
int main(int argc, char **argv)
修改為:
int run(int argc, char **argv)
為了能夠重復使用命令争剿,我們需要修改ffmpeg的清理方法ffmpeg_cleanup,在方法的末尾將一些參數(shù)重置痊末,如下所示:
av_freep(&vstats_filename);
vstats_filename = NULL;
av_freep(&input_streams);
input_streams = NULL;
nb_input_streams = 0;
av_freep(&input_files);
input_files = NULL;
nb_input_files = 0;
av_freep(&output_streams);
output_streams = NULL;
nb_output_streams = 0;
av_freep(&output_files);
output_files = NULL;
nb_input_files = 0;
將 sigterm_handler 方法中的
exit(123)
修改為
exit_program(123);
在ffmpeg.h 文件的末尾蚕苇,添加聲明
int run(int argc, char **argv);
另外在頭文件開頭聲明Android的Log方法:
#include <android/log.h>
#ifndef LOG_TAG
#define LOG_TAG "FFMPEG"
#endif
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG , LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO , LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN , LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG, __VA_ARGS__)
至此凿叠,ffmpeg.c/ffmpeg.h 主文件已經修改完成
3捆蜀、修改 cmdutils.c 文件,添加頭文件和聲明:
#include <setjmp.h>
extern jmp_buf jmp_exit;
修改 exit_program() 方法幔嫂,如下所示:
void exit_program(int ret)
{
if (program_exit)
{
av_log(NULL, AV_LOG_INFO, "run program_exit.\n");
program_exit(ret);
}
else
{
av_log(NULL, AV_LOG_INFO, "program_exit is null\n");
}
// 轉換錯誤碼11辆它,因為ffmpeg命令執(zhí)行成功返回是1,命令參數(shù)錯誤返回的錯誤碼也是1
if (ret == 1)
{
ret = 11;
}
av_log(NULL, AV_LOG_INFO, "exit_program code: %d\n", ret);
longjmp(jmp_exit, ret);
// exit(ret);
}
在cmdutils.h文件中添加以下聲明:
#ifdef FFMPEG_RUN_LIB
#ifdef ANDROID
#include <android/log.h>
#ifndef LOG_TAG
#define LOG_TAG "FFMPEG"
#endif
#define XLOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define XLOGD(...) __android_log_print(ANDROID_LOG_DEBUG , LOG_TAG, __VA_ARGS__)
#define XLOGI(...) __android_log_print(ANDROID_LOG_INFO , LOG_TAG, __VA_ARGS__)
#define XLOGW(...) __android_log_print(ANDROID_LOG_WARN , LOG_TAG, __VA_ARGS__)
#define XLOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG, __VA_ARGS__)
#else
#include <stdio.h>
#define XLOGV(format, ...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG ": " format "\n", ##__VA_ARGS__)
#define XLOGD(format, ...) __android_log_print(ANDROID_LOG_DEBUG , LOG_TAG ": " format "\n", ##__VA_ARGS__)
#define XLOGI(format, ...) __android_log_print(ANDROID_LOG_INFO , LOG_TAG ": " format "\n", ##__VA_ARGS__)
#define XLOGW(format, ...) __android_log_print(ANDROID_LOG_WARN , LOG_TAG ": " format "\n", ##__VA_ARGS__)
#define XLOGE(format, ...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG ": " format "\n", ##__VA_ARGS__)
#endif // ANDROID
#endif // FFMPEG_RUN_LIB
到這里我們就已經把ffmpeg命令工具移植到了項目中履恩,但是我們還不能使用锰茉。接下來,我們需要添加自己的jni調用方法切心。
4飒筑、在src/main/cpp目錄下新建一個的ffmpeg_cmd的文件夾片吊,新建ffmpeg_cmd.c/ffmpeg_cmd.h 、ffmpeg_cmd_wrapper.c/ffmpeg_cmd_wrapper.h文件协屡,用來寫我們的jni控制調用ffmpeg命令工具俏脊。
ffmpeg_cmd.h內容如下:
#ifndef CAINCAMERA_FFMPEG_CMD_H
#define CAINCAMERA_FFMPEG_CMD_H
int run_cmd(int argc, char** argv);
#endif //CAINCAMERA_FFMPEG_CMD_H
ffmpeg_cmd.c內容如下:
#include <setjmp.h>
#include <android/log.h>
#include "ffmpeg_cmd.h"
#include "ffmpeg.h"
#ifdef __cplusplus
extern "C" {
#endif
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "FFMPEG", __VA_ARGS__)
jmp_buf jmp_exit;
int run_cmd(int argc, char** argv)
{
int res = 0;
if(res = setjmp(jmp_exit))
{
LOGD("res=%d", res);
return res;
}
res = run(argc, argv);
LOGD("res_run=%d", res);
return res;
}
#ifdef __cplusplus
}
#endif
ffmpeg_cmd_wrapper.h內容如下:
#ifndef CAINCAMERA_FFMPEG_CMD_WRAPPER_H
#define CAINCAMERA_FFMPEG_CMD_WRAPPER_H
#include"jni.h"
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jint
JNICALL Java_com_cgfay_caincamera_jni_FFmpegCmd_run
(JNIEnv *env, jclass obj, jobjectArray commands);
#ifdef __cplusplus
}
#endif
#endif //CAINCAMERA_FFMPEG_CMD_WRAPPER_H
ffmpeg_cmd_wrapper.c內容如下:
#include "ffmpeg_cmd.h"
#include "ffmpeg_cmd_wrapper.h"
#include "jni.h"
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jint
JNICALL Java_com_cgfay_caincamera_jni_FFmpegCmd_run
(JNIEnv *env, jclass obj, jobjectArray commands)
{
int argc = (*env)->GetArrayLength(env, commands);
char *argv[argc];
jstring jstr[argc];
int i = 0;;
for (i = 0; i < argc; i++)
{
jstr[i] = (jstring)(*env)->GetObjectArrayElement(env, commands, i);
argv[i] = (char *) (*env)->GetStringUTFChars(env, jstr[i], 0);
}
int status = run_cmd(argc, argv);
for (i = 0; i < argc; ++i)
{
(*env)->ReleaseStringUTFChars(env, jstr[i], argv[i]);
}
return status;
}
#ifdef __cplusplus
}
#endif
其中,Java_com_cgfay_caincamera_jni_FFmpegCmd_run 跟java中的jni調用方法要對上肤晓,這里可以換成你自己的包路徑和方法名爷贫。接下來我們java層新建一個FFmpegCmd.java文件。文件內容如下:
package com.cgfay.caincamera.jni;
import android.util.Log;
import com.cgfay.caincamera.utils.FileUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* 用于管理FFmpeg命令工具
* Created by cain.huang on 2017/12/12.
*/
public class FFmpegCmd {
private static final String TAG = "FFmpegCmd";
private static boolean VERBOSE = false;
private static final int RUN_SUCCESS = 0;
private static final int RUN_FAILED = 1;
// 是否正在運行命令
private static boolean mIsRunning = false;
private static final String STR_DEBUG_PARAM = "-d";
static {
System.loadLibrary("ffmpeg");
System.loadLibrary("ffmpeg_cmd");
}
private native static int run(String[] cmd);
public interface OnCompletionListener {
void onCompletion(boolean result);
}
private static int runSafely(String[] cmd) {
int result = -1;
long time = System.currentTimeMillis();
try {
result = run(cmd);
if (VERBOSE) {
Log.d(TAG, "time = " + (System.currentTimeMillis() - time));
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
private static void runSync(ArrayList<String> cmds, final OnCompletionListener listener) {
if (VERBOSE) {
cmds.add(STR_DEBUG_PARAM);
}
final String[] commands = cmds.toArray(new String[cmds.size()]);
Runnable runnable = new Runnable() {
@Override
public void run() {
int result = runSafely(commands);
callbackResult(result, listener);
}
};
mIsRunning = true;
new Thread(runnable).start();
}
private static void callbackResult(int result, OnCompletionListener listener) {
if (VERBOSE) {
Log.d(TAG, "result = " + result);
}
if (listener != null) {
listener.onCompletion(result == 1);
}
mIsRunning = false;
}
/**
* 音視頻混合
* @param srcVideo 視頻路徑
* @param videoVolume 視頻聲音
* @param srcAudio 音頻路徑
* @param audioVolume 音頻聲音
* @param desVideo 目標視頻
* @param callback 狀態(tài)回調
* @return
*/
public static boolean AVMuxer(String srcVideo, float videoVolume,
String srcAudio, float audioVolume, String desVideo,
OnCompletionListener callback) {
if (srcAudio == null || srcAudio.length() <= 0
|| desVideo == null || desVideo.length() <= 0) {
return false;
}
ArrayList<String> cmds = new ArrayList<>();
cmds.add("ffmpeg");
cmds.add("-i");
cmds.add(srcVideo);
cmds.add("-i");
cmds.add(srcAudio);
cmds.add("-c:v");
cmds.add("copy");
cmds.add("-map");
cmds.add("0:v:0");
cmds.add("-strict");
cmds.add("-2");
if (videoVolume <= 0.001f) { // 使用audio聲音
cmds.add("-c:a");
cmds.add("aac");
cmds.add("-map");
cmds.add("1:a:0");
cmds.add("-shortest");
if (audioVolume < 0.99 || audioVolume > 1.01) {
cmds.add("-vol");
cmds.add(String.valueOf((int)(audioVolume * 100)));
}
} else if (videoVolume > 0.001f && audioVolume > 0.001f) { // 混合音視頻聲音
cmds.add("-filter_complex");
cmds.add(String.format(
"[0:a]aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo,volume=%f[a0]; " +
"[1:a]aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo,volume=%f[a1];" +
"[a0][a1]amix=inputs=2:duration=first[aout]", videoVolume, audioVolume));
cmds.add("-map");
cmds.add("[aout]");
} else {
Log.w(TAG, String.format(Locale.getDefault(),
"Illigal volume : SrcVideo = %.2f, SrcAudio = %.2f",
videoVolume, audioVolume));
if (callback != null) {
callback.onCompletion(RUN_FAILED == 1);
}
}
cmds.add("-f");
cmds.add("mp4");
cmds.add("-y");
cmds.add("-movflags");
cmds.add("faststart");
cmds.add(desVideo);
runSync(cmds, callback);
return true;
}
/**
* 設置播放速度
* @param srcVideo
* @param speed
* @param desVideo
* @param callback
*/
public static void setPlaybackSpeed(String srcVideo, float speed,
String desVideo, OnCompletionListener callback) {
ArrayList<String> cmds = new ArrayList<>();
cmds.add("ffmpeg");
cmds.add("-i");
cmds.add(srcVideo);
cmds.add("-y");
cmds.add("-filter_complex");
cmds.add("[0:v]setpts=" + speed + "*PTS[v];[0:a]atempo=" + 1 / speed + "[a]");
cmds.add("-map");
cmds.add("[v]");
cmds.add("-map");
cmds.add("[a]");
cmds.add(desVideo);
runSync(cmds, callback);
}
/**
* 剪切視頻
* @param srcVideo
* @param desVideo
* @param startTime
* @param endTime
* @return
*/
public static boolean cutVideo(String srcVideo, String desVideo,
float startTime, float endTime) {
ArrayList<String> cmds = new ArrayList<>();
cmds.add("ffmpeg");
cmds.add("-i");
cmds.add(srcVideo);
cmds.add("-y");
cmds.add("-ss");
cmds.add("" + startTime);
cmds.add("-t");
cmds.add("" + endTime);
cmds.add("-c");
cmds.add("copy");
cmds.add(desVideo);
String[] commands = cmds.toArray(new String[cmds.size()]);
int result = runSafely(commands);
return (result == 1);
}
/**
* 將圖片轉成視頻
* @param picPath 圖片路徑
* @param duration 時間
* @param desVideo 輸出視頻路徑
* @param callback 回調
*/
public static void convertPictureToVideo(String picPath, float duration,
String desVideo, OnCompletionListener callback) {
ArrayList<String> cmds = new ArrayList<>();
cmds.add("ffmpeg");
cmds.add("-y");
cmds.add("-loop");
cmds.add("1");
cmds.add("-f");
cmds.add("image2");
cmds.add("-i");
cmds.add(picPath);
cmds.add("-t");
cmds.add(""+duration);
cmds.add("-r");
cmds.add("15");
cmds.add(desVideo);
runSync(cmds, callback);
}
/**
* 添加Gif到視頻
* @param videoPath
* @param gifPath
* @param x
* @param y
* @param startTime
* @param desVideo
* @param callback
*/
public static void addGifToVideo(String videoPath, String gifPath,
float x, float y,
float startTime, String desVideo,
OnCompletionListener callback) {
ArrayList<String> cmds = new ArrayList<>();
cmds.add("ffmpeg");
cmds.add("-y");
cmds.add("-i");
cmds.add(videoPath);
// cmds.add("-ignore_loop");
// cmds.add("0");
cmds.add("-i");
cmds.add(gifPath);
cmds.add("-ss");
cmds.add("" + startTime);
cmds.add("-filter_complex");
cmds.add("overlay=" + x + ":" + y);
cmds.add(desVideo);
runSync(cmds, callback);
}
/**
* 旋轉視頻
* @param srcVideo
* @param desVideo
* @param callback
*/
public static void rotateVideo(String srcVideo, String desVideo,
OnCompletionListener callback) {
ArrayList<String> cmds = new ArrayList<>();
cmds.add("ffmpeg");
cmds.add("-i");
cmds.add(srcVideo);
cmds.add("-vf");
// cmds.add("transpose=1:portrait");
cmds.add("rotate=PI/2");
cmds.add(desVideo);
runSync(cmds, callback);
}
/**
* 添加水印
* @param srcVideo
* @param waterMark
* @param desVideo
* @param callback
*/
public static void addWaterMark(String srcVideo, String waterMark,
String desVideo, OnCompletionListener callback) {
ArrayList<String> cmds = new ArrayList<>();
cmds.add("ffmpeg");
cmds.add("-i");
cmds.add(srcVideo);
cmds.add("-i");
cmds.add(waterMark);
cmds.add("-y");
cmds.add("-filter_complex");
cmds.add("[0:v][1:v]overlay=main_w-overlay_w-10:main_h-overlay_h-10[out]"); // 位置
cmds.add("-map");
cmds.add("[out]");
cmds.add("-map");
cmds.add("0:a");
cmds.add("-codec:a"); // keep audio
cmds.add("copy");
cmds.add(desVideo);
runSync(cmds, callback);
}
/**
* 將視頻轉成Gif
* @param videoPath 視頻路徑
* @param gifPath gif路徑
* @param callback 回調
*/
public static void convertVideoToGif(String videoPath, String gifPath,
OnCompletionListener callback) {
ArrayList<String> cmds = new ArrayList<>();
cmds.add("ffmpeg");
cmds.add("-i");
cmds.add(videoPath);
cmds.add("-f");
cmds.add("gif");
cmds.add(gifPath);
runSync(cmds, callback);
}
/**
* 合并多個視頻
* @param videoPathList 視頻列表
* @param desVideo 輸出視頻
* @return
*/
public static boolean combineVideo(List<String> videoPathList, String desVideo) {
String tmpFile = "/sdcard/videolist.txt";
String content = "ffconcat version 1.0\n";
for (String path : videoPathList) {
content += "\nfile " + path;
}
FileUtils.writeFile(tmpFile, content, false);
ArrayList<String> cmds = new ArrayList<>();
cmds.add("ffmpeg");
cmds.add("-y");
cmds.add("-safe");
cmds.add("0");
cmds.add("-f");
cmds.add("concat");
cmds.add("-i");
cmds.add(tmpFile);
cmds.add("-c");
cmds.add("copy");
cmds.add(desVideo);
if (VERBOSE) {
cmds.add(STR_DEBUG_PARAM);
}
String[] commands = cmds.toArray(new String[cmds.size()]);
int result = runSafely(commands);
FileUtils.deleteFile(tmpFile);
return result == 1;
}
/**
* 檢測視頻文件是否正確
* @param videoPath 視頻路徑
* @param time 時間
* @param picPath
* @param callback
*/
public static void getVideoShoot(String videoPath, float time, String picPath,
OnCompletionListener callback) {
ArrayList<String> cmds = new ArrayList<>();
cmds.add("ffmpeg");
cmds.add("-y");
cmds.add("-ss");
cmds.add("" + time);
cmds.add("-i");
cmds.add(videoPath);
cmds.add("-r");
cmds.add("1");
cmds.add("-vframes");
cmds.add("1");
// cmds.add("-vf");
// cmds.add("select=eq(pict_type\\,I)");
cmds.add("-an");
cmds.add("-f");
cmds.add("mjpeg");
cmds.add(picPath);
runSync(cmds, callback);
}
/**
* 裁剪視頻
* @param srcPath 視頻路徑
* @param x x起始坐標
* @param y y起始坐標
* @param width 寬度
* @param height 高度
* @param destPath 目標路徑
* @param callback 回調
*/
public static void cropVideo(String srcPath, int x, int y, int width, int height,
String destPath, OnCompletionListener callback) {
ArrayList<String> cmds = new ArrayList<>();
cmds.add("ffmpeg");
cmds.add("-y");
cmds.add("-i");
cmds.add(srcPath);
cmds.add("-filter:v");
cmds.add("crop=" + width + ":" + height + ":" + x + ":" + y);
cmds.add(destPath);
runSync(cmds, callback);
}
}
至此补憾,我們就已經編寫好我們的ffmpeg命令工具了漫萄。但到了這里,還不能執(zhí)行盈匾。因為我們還沒有在CmakeLists.txt中添加對應的文件腾务,如下:
# 設置cmake最低版本
cmake_minimum_required(VERSION 3.4.1)
# 設置路徑
set(distribution_DIR ${CMAKE_SOURCE_DIR}/../../../../libs)
# 加載ffmpeg庫
add_library( ffmpeg
SHARED
IMPORTED )
set_target_properties( ffmpeg
PROPERTIES IMPORTED_LOCATION
../../../../libs/armeabi-v7a/libffmpeg.so )
# 加載頭文件
# 這里需要添加ffmpeg庫的路徑,用來編譯生成ffmpeg_cmd.so
# 這里如果不引入頭文件削饵,會出現(xiàn)libavresample/avresample.h等頭文件找不到的情況
include_directories(D:/FFmpeg/ffmpeg-3.3.3
src/main/cpp/ffmpeg
src/main/cpp/ffmpeg/include )
# 添加自身的jni庫
add_library( ffmpeg_cmd
SHARED
src/main/cpp/ffmpeg/cmdutils.c
src/main/cpp/ffmpeg/ffmpeg.c
src/main/cpp/ffmpeg/ffmpeg_opt.c
src/main/cpp/ffmpeg/ffmpeg_filter.c
src/main/cpp/ffmpeg_cmd/ffmpeg_cmd.c
src/main/cpp/ffmpeg_cmd/ffmpeg_cmd_wrapper.c )
# 查找Android存在的庫
find_library( log-lib
log )
set(CMAKE_EXE_LINKER_FLAGS "-lz -ldl")
# 鏈接庫文件
target_link_libraries(
ffmpeg_cmd
# ffmpeg庫
ffmpeg
${log-lib} )
sync之后岩瘦,如果在include_directories 中沒有包含ffmpeg庫的目錄,會提示出錯窿撬,找不到路徑:
原因是我們沒有引入頭文件启昧,前面復制到cpp目錄下的頭文件不全陨囊,我們并沒有用到相應的庫思犁,因此打包出來的結果是沒有相應的頭文件的匹层,比如libavresample喧枷,我們用了libswresample段多。因此這里需要引入ffmpeg庫的頭文件路徑進行編譯轮傍。
我們編譯一個apk包氓栈,解壓之后彬犯,可以看到 libffmpeg_cmd.so已經生成赡模。經過裁剪后的 libffmpeg.so 和 libffmpeg_cmd.so 包體積分別之后3.5MB 和 200K田炭,release 包的體積更小。打包得到libffmpeg_cmd.so之后漓柑,我們就可以從CmakeList.txt中注釋掉編譯libffmpeg_cmd.so庫的代碼教硫。然后將libffmpeg_cmd.so復制到libs/armeabi-v7a目錄。
還有什么不明白的辆布,你可以看我的相機項目 CainCamera 是怎么處理的瞬矩。在錄制多段視頻完成后,在預覽頁面點擊保存锋玲,可以看到DCIM目錄下生成了一個合并后的視頻景用,這個就是用了ffmpeg命令執(zhí)行得到。截止本篇文章發(fā)布前惭蹂,相機項目的多段視頻合成功能基本完成伞插,后續(xù)會添加合成進度提示等細節(jié)割粮,歡迎下載體驗和Stars。(12月22日媚污,由于本人發(fā)現(xiàn)FFmpeg命令行在多段視頻合成時舀瓢,如果存在切換濾鏡,會出現(xiàn)合成成功耗美,但播放不了的情況京髓,目前多段視頻合成bu)