音视频开发技术(18)FFmpeg玩转Android视频录制与压缩

您所在的位置:网站首页 ffmpeg录制rtmp 音视频开发技术(18)FFmpeg玩转Android视频录制与压缩

音视频开发技术(18)FFmpeg玩转Android视频录制与压缩

2023-04-14 20:38| 来源: 网络整理| 查看: 265

本文涉及知识点: Andorid 视频和音频采集 YUV视频处理(手动剪切、旋转、镜像等)PCM音频处理 利用FFmpeg API ,YUV编码为H264、PCM编码为AAC FFmpeg 编码器的配置 JNI在工程中的实际运用 Android下FFmpeg命令工具的制作与应用 Android Studio插件 cMake 在工程中的应用

音视频开发知识点路线图:

充能:

至少需要知道YUV、PCM、MP4是什么(视音频编解码技术零基础学习方法)。 最好能先阅读编译Android下可用的FFmpeg(包含libx264与libfdk-aac)、编译Android下可执行命令的FFmpeg、Android下玩JNI的新老三种姿势,为了不太啰嗦,这些文章中分享过的大多数知识将不再重复。 对C/C++基本语法有基本的了解。

环境与工具

系统: macOS-10.12.5 编译器: Android Studio-2.3.2 ndk: r14 FFmpeg: 3.2.5项目概括:

1. 效果图:

2. 整体流程:

3. 工程目录浏览:

新建项目

我们新建一个项目,也许与以往不同,需要勾选上 C++ 支持与 C++ standard选项时选择 C++ 11,如下图:

C++支持是必须的,至于选用C++ 11也是有原因的,后面我们会用的里面的一些API。然后我们把在编译Android下可用的FFmpeg(包含libx264与libfdk-aac)中编译好的六个动态库、头文件还有 cmdutils.c cmdutils.h cmdutils_common_opts.h config.h ffmpeg.c ffmpeg.h ffmpeg_filter.c ffmpeg_opt.c copy到我们工程的 cpp目录下,完成后你cpp目录应该如下

也许你会比我多一个自动生成的native-lib.cpp,这个文件暂时保留它。

相关视频推荐:

LinuxC++音视频开发视频:免费】FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发

【文章福利】:音视频面试题、学习资料、教学视频和学习路线图资料(资料包括C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等),qun994289133免费分享,有需要的可以加群领取哦!~学习交流裙994289133加入领取资料

企鹅群994289133领取资料企鹅群994289133领取资料编写JNI接口:

我新建了一个接口类FFmpegBridge.java,且根据我的需求暂时定义了如下方法:

package com.mabeijianxi.smallvideorecord2.jniinterface;import java.util.ArrayList;/** * Created by jianxi on 2017/5/12. * https://github.com/mabeijianxi * [email protected] */public class FFmpegBridge { private static ArrayList listeners=new ArrayList(); static { System.loadLibrary("avutil"); System.loadLibrary("swresample"); System.loadLibrary("avcodec"); System.loadLibrary("avformat"); System.loadLibrary("swscale"); System.loadLibrary("avfilter"); System.loadLibrary("jx_ffmpeg_jni"); } /** * 结束录制并且转码保存完成 */ public static final int ALL_RECORD_END =1; public final static int ROTATE_0_CROP_LF=0; /** * 旋转90度剪裁左上 */ public final static int ROTATE_90_CROP_LT =1; /** * 暂时没处理 */ public final static int ROTATE_180=2; /** * 旋转270(-90)裁剪左上,左右镜像 */ public final static int ROTATE_270_CROP_LT_MIRROR_LR=3; /** * * @return 返回ffmpeg的编译信息 */ public static native String getFFmpegConfig(); /** * 命令形式运行ffmpeg * @param cmd * @return 返回0表示成功 */ private static native int jxCMDRun(String cmd[]); /** * 编码一帧视频,暂时只能编码yv12视频 * @param data * @return */ public static native int encodeFrame2H264(byte[] data); /** * 编码一帧音频,暂时只能编码pcm音频 * @param data * @return */ public static native int encodeFrame2AAC(byte[] data); /** * 录制结束 * @return */ public static native int recordEnd(); /** * 初始化 * @param debug * @param logUrl */ public static native void initJXFFmpeg(boolean debug,String logUrl); public static native void nativeRelease(); /** * * @param mediaBasePath 视频存放目录 * @param mediaName 视频名称 * @param filter 旋转镜像剪切处理 * @param in_width 输入视频宽度 * @param in_height 输入视频高度 * @param out_height 输出视频高度 * @param out_width 输出视频宽度 * @param frameRate 视频帧率 * @param bit_rate 视频比特率 * @return */ public static native int prepareJXFFmpegEncoder(String mediaBasePath, String mediaName, int filter,int in_width, int in_height, int out_width, int out_height, int frameRate, long bit_rate); /** * 命令形式执行 * @param cmd */ public static int jxFFmpegCMDRun(String cmd){ String regulation="[ \\t]+"; final String[] split = cmd.split(regulation); return jxCMDRun(split); } /** * 底层回调 * @param state * @param what */ public static synchronized void notifyState(int state,float what){ for(FFmpegStateListener listener: listeners){ if(listener!=null){ if(state== ALL_RECORD_END){ listener.allRecordEnd(); } } } } /** *注册录制回调 * @param listener */ public static void registFFmpegStateListener(FFmpegStateListener listener){ if(!listeners.contains(listener)){ listeners.add(listener); } } public static void unRegistFFmpegStateListener(FFmpegStateListener listener){ if(listeners.contains(listener)){ listeners.remove(listener); } } public interface FFmpegStateListener { void allRecordEnd(); } }

你新建这些方法的时候由于native没有定义,这时候它们都会爆红,不要担心不要纠结,光标放到对应的方法上,轻轻按下Atl + Enter你就会出现如图的效果了:

再次确定之后这个接口就会在native添加。我不太喜欢叫native-lib.cpp,于是我改成了jx_ffmpeg_jni.cpp编写native代码

我用c/c++用的不多,Java又用习惯了,所以在命名上有时候很纠结,看不惯亲的怎么办?那就些许的忍一忍吧~~ 1. 准备log函数:

不管玩什么语言,没日志玩毛线啊,所以这是第一步。新建jx_log.cpp与jx_log.h。jx_log.h:

/** * Created by jianxi on 2017/6/2. * https://github.com/mabeijianxi * [email protected] */#ifndef JIANXIFFMPEG_JX_LOG_H#define JIANXIFFMPEG_JX_LOG_H#include extern int JNI_DEBUG;#define LOGE(debug, format, ...) if(debug){__android_log_print(ANDROID_LOG_ERROR, "jianxi_ffmpeg", format, ##__VA_ARGS__);}#define LOGI(debug, format, ...) if(debug){__android_log_print(ANDROID_LOG_INFO, "jianxi_ffmpeg", format, ##__VA_ARGS__);}#endif //JIANXIFFMPEG_JX_LOG_H

jx_log.cpp:

/** * Created by jianxi on 2017/6/2. * https://github.com/mabeijianxi * [email protected] */#include "jx_log.h"int JNI_DEBUG= 1;

当然我们也定义了一个是否开启debug的标志 JNI_DEBUG。

2.准备好可执行命令的FFmpeg接口:

这里假设你已经看完了编译Android下可执行命令的FFmpeg,因为我们要对之前copy进来的源码做些修改,不然没法用的。我们新建两个文件来对接FFmpeg,文件中一个函数给Java层调用,一个给Native调用,还有一个是初始化debug控制日志用的,可以先不管。

jx_ffmpeg_cmd_run.h:

/** * Created by jianxi on 2017/6/4. * https://github.com/mabeijianxi * [email protected] */#ifndef JIANXIFFMPEG_FFMPEG_RUN_H#define JIANXIFFMPEG_FFMPEG_RUN_H#include JNIEXPORT jint JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_jxCMDRun(JNIEnv *env, jclass type, jobjectArray commands);void log_callback(void* ptr, int level, const char* fmt, va_list vl);JNIEXPORT void JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_initJXFFmpeg(JNIEnv *env, jclass type, jboolean debug, jstring logUrl_);int ffmpeg_cmd_run(int argc, char **argv);#endif //JIANXIFFMPEG_FFMPEG_RUN_H

jx_ffmpeg_cmd_run.c:

/** * Created by jianxi on 2017/6/4.. * https://github.com/mabeijianxi * [email protected] */#include "jx_ffmpeg_cmd_run.h"#include "ffmpeg.h"#include "jx_log.h"/** * 以命令行方式运行,返回0表示成功 */JNIEXPORT jint JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_jxCMDRun(JNIEnv *env, jclass type, jobjectArray commands){ 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); } return ffmpeg_cmd_run(argc,argv); }int ffmpeg_cmd_run(int argc, char **argv){ return jxRun(argc, argv); }char *logUrl;/** * 初始化debug工具 */JNIEXPORT void JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_initJXFFmpeg(JNIEnv *env, jclass type, jboolean debug, jstring logUrl_) { JNI_DEBUG = debug; if (JNI_DEBUG&&logUrl_!=NULL) { av_log_set_callback(log_callback); const char* log = (*env)->GetStringUTFChars(env,logUrl_, 0); logUrl = (char*)malloc(strlen(log)); strcpy(logUrl,log); (*env)->ReleaseStringUTFChars(env,logUrl_, log); } }void log_callback(void *ptr, int level, const char *fmt, va_list vl) { FILE *fp = NULL; if (!fp) fp = fopen(logUrl, "a+"); if (fp) { vfprintf(fp, fmt, vl); fflush(fp); fclose(fp); } }

一口气写到这里,必定会四处爆红,惨不忍睹,各种找不到文件,找不到方法,那是因为你添加了这么多文件,cMake工具不知道,正确的做法是每添加一个C/C++文件然后就去 CMakeLists.txt 里面告诉人家一声,完了还别忘了点击 Sync 同步下子。

3. 准备一个安全的队列:

我们在采集音视频数据后会发送给FFmpeg做一系列的处理,由于是软编码所以编码快慢和CPU有很大的关系,就现在的x264的算法结合当今的CPU是跟不上咋们采集每秒20帧+的速度的,直接采集一帧就编码一帧的话肯定会丢帧的,所以我决定把它放入一个队里里面,由于存在多线程编程,我们的队列需要 safety,就跟几个男的抢一个妹子一样,妹子自然需要我这样的人保护她咯。这个队列的代码是我网上copy的,没啥说的~~

threadsafe_queue.cpp

/** * Created by jianxi on 2017/5/31. * https://github.com/mabeijianxi * [email protected] */#ifndef JIANXIFFMPEG_THREADSAFE_QUEUE_CPP#define JIANXIFFMPEG_THREADSAFE_QUEUE_CPP#include #include #include #include /** * 一个安全的队列 */templateclass threadsafe_queue {private: mutable std::mutex mut; std::queue data_queue; std::condition_variable data_cond;public: threadsafe_queue() {} threadsafe_queue(threadsafe_queue const &other) { std::lock_guard lk(other.mut); data_queue = other.data_queue; } void push(T new_value)//入队操作 { std::lock_guard lk(mut); data_queue.push(new_value); data_cond.notify_one(); } void wait_and_pop(T &value)//直到有元素可以删除为止 { std::unique_lock lk(mut); data_cond.wait(lk, [this] { return !data_queue.empty(); }); value = data_queue.front(); data_queue.pop(); } std::shared_ptr wait_and_pop() { std::unique_lock lk(mut); data_cond.wait(lk, [this] { return !data_queue.empty(); }); std::shared_ptr res(std::make_shared(data_queue.front())); data_queue.pop(); return res; } bool try_pop(T &value)//不管有没有队首元素直接返回 { std::lock_guard lk(mut); if (data_queue.empty()) return false; value = data_queue.front(); data_queue.pop(); return true; } std::shared_ptr try_pop() { std::lock_guard lk(mut); if (data_queue.empty()) return std::shared_ptr(); std::shared_ptr res(std::make_shared(data_queue.front())); data_queue.pop(); return res; } bool empty() const { return data_queue.empty(); } };#endif //JIANXIFFMPEG_THREADSAFE_QUEUE_CPP

这里面用的几个 lib 就是 C++ 11标准里面的啦~

4. 准备一个储存配置信息的结构体:

其实这玩意和JavaBean差不多嘛,直接搞代码,代码中的JXJNIHandler字段姑且当做没看到。

jx_user_arguments.h:

/** * Created by jianxi on 2017/5/26. * https://github.com/mabeijianxi * [email protected] */#ifndef JIANXIFFMPEG_JX_USER_ARGUMENTS_H#define JIANXIFFMPEG_JX_USER_ARGUMENTS_H#include "jni.h"class JXJNIHandler;typedef struct UserArguments { const char *media_base_path; //文件储存地址 const char *media_name; // 文件命令前缀 char *video_path; //视频储存地址 char *audio_path; //音频储存地址 char *media_path; //合成后的MP4储存地址 int in_width; //输出宽度 int in_height; //输入高度 int out_height; //输出高度 int out_width; //输出宽度 int frame_rate; //视频帧率控制 long long video_bit_rate; //视频比特率控制 int audio_bit_rate; //音频比特率控制 int audio_sample_rate; //音频采样率控制(44100) int v_custom_format; //一些滤镜操作控制 JNIEnv *env; //env全局指针 JavaVM *javaVM; //jvm指针 jclass java_class; //java接口类的calss对象 JXJNIHandler *handler; // 一个全局处理对象的指针} ;#endif //JIANXIFFMPEG_JX_USER_ARGUMENTS_H

这个结构体在整个过程中都会用到。

5. 编写视频(YUV)编码代码

这小节是本文的核心之一,简化后的思路是这样的:

有的兄弟可能会问为什么不编码一帧合成一帧,因为啊我测试了下合成时间,基本都是毫秒级别的,还有就是嫌麻烦,我这样做的话直接用我们制作的FFmpeg命令工具然后几行代码就搞定了 代码贴完了,现在来听本屌说说它的前世今生,很关键~。

1)视频编码器参数配置

这里稍微说几个重要的,一会没吐槽到的参数可以再开这里再仔细看看,ffmpeg 编码器AVCodecContext 的配置参数。

size_t path_length = strlen(arguments->video_path); char *out_file = (char *) malloc(path_length + 1); strcpy(out_file, arguments->video_path);

通过上面代码我们copy了下视频输出地址,我们视频输出地址是以.h264结尾的很关键,因为下面的 avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, out_file)函数会检查其合法性,并且根据你的后缀格式对应为 pFormatCtx 赋值。

pCodecCtx->codec_id = AV_CODEC_ID_H264 这里指定编码器id,是H264无疑; pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;指定编码的数据格式; pCodecCtx->bit_rate = arguments->video_bit_rate,指定视频比特率,这个参数相当重要,很大程度上决定你视频质量与大小,但是根据这个也跟码率模式有关在VBR模式下,其将会有一定的波动。 pCodecCtx->thread_count = 16 线程条数,我这里写死了,不太好,道上的朋友称1.5陪核数就好。 pCodecCtx->time_base.num = 1; pCodecCtx->time_base.den = arguments->frame_rate 这两个是控制帧率的,num是分母,den是分子,相除既得到帧率。你必须和你采集到的帧率一样,你这里很关键,不然可能会导致视音不同步,踩坑的路过~,你给你相机设置的帧数不一定就是实际保存的帧数,这个时候也会造成视音不同步,这个后面与Java层对接的时候再道来。 av_opt_set(pCodecCtx->priv_data, "preset", "superfast", 0) 这里是指定一个编码速度的预设值,我暂时写死为最快。 pCodecCtx->qmin pCodecCtx->qmax 这是量化范围设定,其值范围为0~51,越小质量越高,需要的比特率越大,0为无损编码。关于编码过程及原理可阅读视频压缩编码和音频压缩编码的基本原理 pCodecCtx->max_b_frames = 3 最大b帧是3,可以设置为0这样编码时会快一些,因为运动估计和运动补偿编码时分 i、b、p帧,借鉴一句雷神的话:I帧只使用本帧内的数据进行编码,在编码过程中它不需要进行运动估计和运动补偿。显然,由于I帧没有消除时间方向的相关性,所以压缩比相对不高。P帧在编码过程中使用一个前面的I帧或P帧作为参考图像进行运动补偿,实际上是对当前图像与参考图像的差值进行编码。B帧的编码方式与P帧相似,惟一不同的地方是在编码过程中它要使用一个前面的I帧或P帧和一个后面的I帧或P帧进行预测。由此可见,每一个P帧的编码需要利用一帧图像作为参考图像,而B帧则需要两帧图像作为参考。相比之下,B帧比P帧拥有更高的压缩比,所以b帧多会有一定延迟。 av_dict_set(¶m, "profile", "baseline", 0) 它可以将你的输出限制到一个特定的 H.264 profile,所有profile 包括:baseline,main.high,high10,high422,high444 ,注意使用--profile选项和无损编码是不兼容的。

2)Android摄像头所采集的YUV数据结构

先简要说说YUV格式,与RGB类似YUV也是一种颜色编码方法,Y:表示明亮度(Luminance或Luma),也就是灰度值;而 U 和 V :表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。如果只有Y那么就是黑白音像。根据采样方式不同主要有YUV4:4:4,YUV4:2:2,YUV4:2:0。其YUV 4:4:4采样,每一个Y对应一组UV分量。 YUV 4:2:2采样,每两个Y共用一组UV分量。YUV 4:2:0采样,每四个Y共用一组UV分量 。举个例子,屏幕上有八个像素点,YUV4:4:4会有8个Y,8个U,8个V。YUV4:2:2会有8个Y,4个U,4个V。YUV4:2:0会有8个Y,2个U,2个V。我们要对咋们采集的数据做处理,我们必须知道其数据类型和数据结构,在老版本的android sdk中其只能采集两种模式的数据,YV12与NV12,他们都是属于YUV420,只是其排列结构不同。我们看看下面的图,当然下面第一张图我P过,因为原图有错,但是人老了手斗没P完美,就将就看了。

可以看到Y1, Y2, Y7, Y8这些物理上相近的4个像素公用了同样的U1和V1,相似的Y3,Y4,Y9,Y10用的就是U2和V2。这里不同的颜色把这个特性刻画的非常形象,一 目了然。格子数目就是这一帧图像的byte数组的大小,其数组元素排放顺序就是后面那一长条的样子。

NV12如下:

可以发现它们只是UV的排放位置不同而已。

3)YV12数据处理

用YV12于NV12都是可以的,我在配置相机参数的时候选择了YV12,接下我们写几个简单的算法实现视频的剪切旋转,非常的简单,我当时估摸着是这个样子就写出来了。 我们这里假设我们采集的视频宽是640,高是480,我们要剪切成宽是400,高是300的视频。根据上面的知识我们能指定640*480的一帧byte数组里面将会有640*480个Y,且排在最前面,然后有(1/4)*640*480个V,然后有(1/4)*640*480个U,我们要剪切成400*300,自然是保留一部分数据即可。我们先对Y建立一个模型,既然是640*480,我们可以把它当成一行有640个Y,一共有480行,如下图所示红色标注内表示640*480个Y,而黄色区域内则是我们剪切完成的Y的所有值。

需要注意图像方向哈。有了这个模型我们就可以写代码操作数组了。下面搞段代码:

剪切Y:

unsigned char *in_buf; unsigned char *out_buf_y; for(int i=480-300;i

320*240的区域就是我们就是我们U值或者V值的区域,200*150的区域就是我们剪切后的U值或者V值的目标区域。代码如下:

剪切UV:

unsigned char *in_buf; unsigned char *out_buf_u; unsigned char *out_buf_v; for(int i=(480-300)/2;i

思路有了,就是如上图所示,我们for循环不变,因为需要剪切的位置不变,我们只改变输出数组的排放位置,原来第一排的放到最后一列,第二排放到倒数第二列,以此内推。下面也用代码演示下:

Y剪切并顺时针旋转90°:

unsigned char *in_buf; unsigned char *out_buf_y; for(int i=(480-300);ichannel_layout = AV_CH_LAYOUT_MONO; pCodecCtx->channels = av_get_channel_layout_nb_channels(pCodecCtx->channel_layout) 这是设置通道数,由于对音频要求不高我采用了单通道,这里也需要和Java音频采集的时候设置的参数对应。还有很多选择如 AV_CH_LAYOUT_STEREO 是立体声双通道,AV_CH_LAYOUT_4POINT0 是4通道。 pCodecCtx->bit_rate = arguments->audio_bit_rate 音频比特率。

配置完参数其他就交给FFmpeg了。

7. 编写视频合成类

在音频和视频都编码完成后,我们需要将其合成mp4,现在就可以用上我们做好的FFmpeg命令工具了,我们只需把地址丢给它即可,这个合成过程也耗时很少。

jx_media_muxer.h:

/** * Created by jianxi on 2017/5/24. * https://github.com/mabeijianxi * [email protected] */#ifndef JIANXIFFMPEG_JX_MEDIA_MUXER_H#define JIANXIFFMPEG_JX_MEDIA_MUXER_H#include "base_include.h"class JXMediaMuxer{public: int startMuxer(const char * video, const char *audio , const char *out_file);private: };#endif //JIANXIFFMPEG_JX_MEDIA_MUXER_H



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3