在前一篇文章windows环境下编译ffmpeg打包成单个so并使用Cmake集成到Android工程中 我们说到了将ffmpeg 编译打包成单个so,并使用cmake 集成到android工程中,现在我们来说说,如何使用cmake 生成能够使用jni 调用 ffmpeg命令工具。首先,你得按照前一篇文章所说的生成了libffmpeg.so包,然后再执行接下来的步骤。
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,在方法的末尾将一些参数重置,如下所示:
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命令执行成功返回是1,命令参数错误返回的错误码也是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 状态回调
* @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命令工具了。但到了这里,还不能执行。因为我们还没有在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
# 这里如果不引入头文件,会出现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命令执行得到。截止本篇文章发布前,相机项目的多段视频合成功能基本完成,后续会添加合成进度提示等细节,欢迎下载体验和Stars。(12月22日,由于本人发现FFmpeg命令行在多段视频合成时,如果存在切换滤镜,会出现合成成功,但播放不了的情况,目前多段视频合成bu)
网友评论