博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
利用FFmpeg玩转Android视频录制与压缩(二)
阅读量:4181 次
发布时间:2019-05-26

本文共 94469 字,大约阅读时间需要 314 分钟。

请尊重原创,转载请注明出处:

预热


时光荏苒,光阴如梭,离上一次吹牛逼已经过去了两三个月,身边很多人的女票已经分了又合,合了又分,本屌依旧骄傲单身。上一次啊我们大致说了一些简单的FFmpeg命令以及层简单的调用方式,然后有很多朋友在github或者csdn上给我留言,很多时候我都选择避而不答,原因是本库以前用的so包是不开源的,我根本改不了里面东西。但是这一次啊我们玩点大的,我重新编译了FFmpeg且重写JNI的接口函数,这次将从CJava全面开源,2.0项目花了本尊两个多月的业余时间,今天终于完工,非常鸡冻,且本博客将抒发出作者的全部心声,有没有很鸡冻,有没有。鸡冻之余,我也想吐槽下其软便的效率,确实不是很高,3.0的时候将会试试硬编码,或则在2.0迭代的时候会采用H265编码,这都是后话了,不过看把小视频换成大视频的节奏,应该可以搞。

本文涉及知识点:
  • Andorid 视频和音频采集
  • YUV视频处理(手动剪切、旋转、镜像等)PCM音频处理
  • 利用FFmpeg API ,YUV编码为H264、PCM编码为AAC
  • FFmpeg 编码器的配置
  • JNI在工程中的实际运用
  • Android下FFmpeg命令工具的制作与应用
  • Android Studio插件 cMake 在工程中的应用
充能:
  • 至少需要知道YUV、PCM、MP4是什么()。
  • 最好能先阅读、、,为了不太啰嗦,这些文章中分享过的大多数知识将不再重复。
  • 对C/C++基本语法有基本的了解。
本人环境与工具
  • 系统: macOS-10.12.5
  • 编译器: Android Studio-2.3.2
  • ndk: r14
  • FFmpeg: 3.2.5

项目概括:

1. 效果图:

项目地址没变:
这里复用了1.0版本的gif图,因为界面一点没变,功能的话暂时没封装那么多,没关系后期会补上。

2. 整体流程:

3. 工程目录浏览:

新建项目


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

C++支持是必须的,至于选用C++ 11也是有原因的,后面我们会用的里面的一些API。
然后我们把在中编译好的六个动态库、头文件还有 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,这个文件暂时保留它。

编写JNI接口:

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

package com.mabeijianxi.smallvideorecord2.jniinterface;import java.util.ArrayList;/** * Created by jianxi on 2017/5/12. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */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,其内容暂时如下:

/** * Created by jianxi on 2017/5/12. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#include 
#include
using namespace std;/** * 编码准备,写入配置信息 */extern "C"JNIEXPORT jint JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_prepareJXFFmpegEncoder(JNIEnv *env, jclass type, jstring media_base_path_, jstring media_name_, jint v_custom_format, jint in_width, jint in_height, jint out_width, jint out_height, jint frame_rate, jlong video_bit_rate) {}/** * 编码一帧视频 */extern "C"JNIEXPORT jint JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_encodeFrame2H264(JNIEnv *env, jclass type, jbyteArray data_) { return 0;}/** * 获取ffmpeg编译信息 */extern "C"JNIEXPORT jstring JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_getFFmpegConfig(JNIEnv *env, jclass type) { return NULL;}/** * 编码一帧音频 */extern "C"JNIEXPORT jint JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_encodeFrame2AAC(JNIEnv *env, jclass type, jbyteArray data_) { return 0;}/** *结束 */extern "C"JNIEXPORT jint JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_recordEnd(JNIEnv *env, jclass type) { return 0;}JNIEXPORT void JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_nativeRelease(JNIEnv *env, jclass type) { // TODO}

编写native代码


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

1. 准备log函数:

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

jx_log.h:

/** * Created by jianxi on 2017/6/2. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#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 * mabeijianxi@gmail.com */#include "jx_log.h"int JNI_DEBUG= 1;

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

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

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

jx_ffmpeg_cmd_run.h:

/** * Created by jianxi on 2017/6/4. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#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 * mabeijianxi@gmail.com */#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. CMakeLists.txt编写:

先强上一个脚本:

# For more information about using CMake with Android Studio, read the# documentation: https://d.android.com/studio/projects/add-native-code.html# Sets the minimum version of CMake required to build the native library.cmake_minimum_required(VERSION 3.4.1)# Creates and names a library, sets it as either STATIC# or SHARED, and provides the relative paths to its source code.# You can define multiple libraries, and CMake builds them for you.# Gradle automatically packages shared libraries with your APK.add_library( # Sets the name of the library.             jx_ffmpeg_jni             # Sets the library as a shared library.             SHARED             # Provides a relative path to your source file(s).              src/main/cpp/jx_yuv_encode_h264.cpp              src/main/cpp/jx_pcm_encode_aac.cpp              src/main/cpp/jx_media_muxer.cpp              src/main/cpp/jx_jni_handler.cpp              src/main/cpp/jx_ffmpeg_jni.cpp              src/main/cpp/threadsafe_queue.cpp              src/main/cpp/jx_log.cpp              src/main/cpp/cmdutils.c              src/main/cpp/ffmpeg.c              src/main/cpp/ffmpeg_filter.c              src/main/cpp/ffmpeg_opt.c              src/main/cpp/jx_ffmpeg_cmd_run.c              src/main/cpp/jx_ffmpeg_config.cpp             )add_library(            avcodec            SHARED            IMPORTED            )set_target_properties(    avcodec    PROPERTIES IMPORTED_LOCATION    ${CMAKE_SOURCE_DIR}/src/main/cpp/libavcodec.so    )add_library(            avfilter            SHARED            IMPORTED             )set_target_properties(        avfilter        PROPERTIES IMPORTED_LOCATION        ${CMAKE_SOURCE_DIR}/src/main/cpp/libavfilter.so        )add_library(            avformat            SHARED            IMPORTED            )set_target_properties(            avformat            PROPERTIES IMPORTED_LOCATION            ${CMAKE_SOURCE_DIR}/src/main/cpp/libavformat.so            )add_library(            avutil            SHARED            IMPORTED            )set_target_properties(            avutil            PROPERTIES IMPORTED_LOCATION            ${CMAKE_SOURCE_DIR}/src/main/cpp/libavutil.so            )add_library(            swresample            SHARED            IMPORTED            )set_target_properties(            swresample            PROPERTIES IMPORTED_LOCATION            ${CMAKE_SOURCE_DIR}/src/main/cpp/libswresample.so             )add_library(            swscale            SHARED            IMPORTED            )set_target_properties(            swscale            PROPERTIES IMPORTED_LOCATION            ${CMAKE_SOURCE_DIR}/src/main/cpp/libswscale.so             )add_library(            jxffmpegcmd            SHARED            IMPORTED            )set_target_properties(            jxffmpegcmd            PROPERTIES IMPORTED_LOCATION            ${CMAKE_SOURCE_DIR}/src/main/cpp/libjxffmpegrun.so             )include_directories(    ${CMAKE_SOURCE_DIR}/ffmpeg-3.2.5/)# Searches for a specified prebuilt library and stores the path as a# variable. Because CMake includes system libraries in the search path by# default, you only need to specify the name of the public NDK library# you want to add. CMake verifies that the library exists before# completing its build.find_library( # Sets the name of the path variable.              log-lib              # Specifies the name of the NDK library that              # you want CMake to locate.              log )# Specifies libraries CMake should link to your target library. You# can link multiple libraries, such as libraries you define in this# build script, prebuilt third-party libraries, or system libraries.target_link_libraries( # Specifies the target library.                       jx_ffmpeg_jni                       avcodec                       avfilter                       avformat                       avutil                       swresample                       swscale                       # Links the target library to the log library                       # included in the NDK.                       ${log-lib} )

当然这个脚本是整个完整工程的,有些文件我们到后面才会建出来,现在就忍耐一下,如果你不想被爆红那么就需要每添加一个文件然后就在第一个 add_library 里面也添加一下,再点击 Studio的同步按钮。 里面其他 library 都是我们事先编译好copy进来的,所以采用预构建的方式添加,这里都是相对路径,所以你不需要修改什么。

include_directories 里面写上你已经编译过的源码的路径,很关键。这里面的头文件才是全的~。

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

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

threadsafe_queue.cpp

/** * Created by jianxi on 2017/5/31. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#ifndef JIANXIFFMPEG_THREADSAFE_QUEUE_CPP#define JIANXIFFMPEG_THREADSAFE_QUEUE_CPP#include 
#include
#include
#include
/** * 一个安全的队列 */template
class 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标准里面的啦~

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

其实这玩意和JavaBean差不多嘛,直接搞代码,代码中的JXJNIHandler

字段姑且当做没看到。

jx_user_arguments.h:

/** * Created by jianxi on 2017/5/26. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#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

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

6. 编写一个base.h

其实啊,当时写这个头文件是不想老去include同样的东西,我们视频编码与音频编码都需要要include的头文件放在了这里,并且定义了一些规则性的宏。

base_include.h:

/** * Created by jianxi on 2017/5/18. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#ifndef JIANXIFFMPEG_BASE_INCLUDE_H#define JIANXIFFMPEG_BASE_INCLUDE_Hextern "C"{#include "include/libavcodec/avcodec.h"#include "include/libavformat/avformat.h"#include "include/libavcodec/avcodec.h"#include "include/libavutil/opt.h"}#include "threadsafe_queue.cpp"#include 
#include
#define END_STATE 1#define START_STATE 0#define ROTATE_0_CROP_LT 0/** * 旋转90度剪裁左上 */#define ROTATE_90_CROP_LT 1/** * 暂时没处理 */#define ROTATE_180 2/** * 旋转270(-90)裁剪左上,左右镜像 */#define ROTATE_270_CROP_LT_MIRROR_LR 3using namespace std;#endif //JIANXIFFMPEG_BASE_INCLUDE_H

FFmpeg源码C的,include时 extern "C"很关键

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

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

有的兄弟可能会问为什么不编码一帧合成一帧,因为啊我了下合成时间,基本都是毫秒级别的,还有就是嫌麻烦,我这样做的话直接用我们制作的FFmpeg命令工具然后几行代码就搞定了,先上代码。

jx_yuv_encode_h264.h:

/** * Created by jianxi on 2017/5/12. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#ifndef JIANXIFFMPEG_JX_YUV_ENCODE_H264_H#define JIANXIFFMPEG_JX_YUV_ENCODE_H264_H#include "base_include.h"#include "jx_user_arguments.h"using namespace std;/** * yuv编码h264 */class JXYUVEncodeH264 {public:    JXYUVEncodeH264(UserArguments* arg);public:    int initVideoEncoder();    static void* startEncode(void * obj);    int startSendOneFrame(uint8_t *buf);    void user_end();    int encodeEnd();    void custom_filter(const JXYUVEncodeH264 *h264_encoder, const uint8_t *picture_buf,                       int in_y_size,                       int format);private:    int flush_encoder(AVFormatContext *fmt_ctx, unsigned int stream_index);private:    UserArguments *arguments;    int is_end = 0;    threadsafe_queue
frame_queue; AVFormatContext *pFormatCtx; AVOutputFormat *fmt; AVStream *video_st; AVCodecContext *pCodecCtx; AVCodec *pCodec; AVPacket pkt; AVFrame *pFrame; int picture_size; int out_y_size; int framecnt = 0; int frame_count = 0; ~JXYUVEncodeH264() { }};#endif //JIANXIFFMPEG_JX_YUV_ENCODE_H264_H

jx_yuv_encode_h264.cpp:

/** * Created by jianxi on 2017/5/12. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#include "jx_yuv_encode_h264.h"#include "jx_jni_handler.h"#include "jx_log.h"#include 
JXYUVEncodeH264::JXYUVEncodeH264(UserArguments *arg) : arguments(arg) {}/** * 结束编码时刷出还在编码器里面的帧 * @param fmt_ctx * @param stream_index * @return */int JXYUVEncodeH264::flush_encoder(AVFormatContext *fmt_ctx, unsigned int stream_index) { int ret; int got_frame; AVPacket enc_pkt; if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities & CODEC_CAP_DELAY)) return 0; while (1) { enc_pkt.data = NULL; enc_pkt.size = 0; av_init_packet(&enc_pkt); ret = avcodec_encode_video2(fmt_ctx->streams[stream_index]->codec, &enc_pkt, NULL, &got_frame); av_frame_free(NULL); if (ret < 0) break; if (!got_frame) { ret = 0; break; } LOGI(JNI_DEBUG, "_Flush Encoder: Succeed to encode 1 frame video!\tsize:%5d\n", enc_pkt.size); /* mux encoded frame */ ret = av_write_frame(fmt_ctx, &enc_pkt); if (ret < 0) break; } return ret;}/** * 初始化视频编码器 * @return */int JXYUVEncodeH264::initVideoEncoder() { LOGI(JNI_DEBUG, "视频编码器初始化开始") size_t path_length = strlen(arguments->video_path); char *out_file = (char *) malloc(path_length + 1); strcpy(out_file, arguments->video_path); av_register_all(); //Method1.// pFormatCtx = avformat_alloc_context();// //Guess Format// fmt = av_guess_format(NULL, out_file, NULL);//// LOGE(JNI_DEBUG,",fmt==null?:%s", fmt == NULL ? "null" : "no_null");// pFormatCtx->oformat = fmt; //Method 2. avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, out_file); fmt = pFormatCtx->oformat; //Open output URL if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0) { LOGE(JNI_DEBUG, "_Failed to open output file! \n"); return -1; } video_st = avformat_new_stream(pFormatCtx, 0); //video_st->time_base.num = 1; //video_st->time_base.den = 25; if (video_st == NULL) { LOGE(JNI_DEBUG, "_video_st==null"); return -1; } //Param that must set pCodecCtx = video_st->codec; //pCodecCtx->codec_id =AV_CODEC_ID_HEVC; pCodecCtx->codec_id = AV_CODEC_ID_H264; pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO; pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P; if (arguments->v_custom_format == ROTATE_0_CROP_LT || arguments->v_custom_format == ROTATE_180) { pCodecCtx->width = arguments->out_width; pCodecCtx->height = arguments->out_height; } else { pCodecCtx->width = arguments->out_height; pCodecCtx->height = arguments->out_width; } pCodecCtx->bit_rate = arguments->video_bit_rate; pCodecCtx->gop_size = 250; pCodecCtx->thread_count = 16; pCodecCtx->time_base.num = 1; pCodecCtx->time_base.den = arguments->frame_rate;// pCodecCtx->me_pre_cmp = 1; //H264 //pCodecCtx->me_range = 16; //pCodecCtx->max_qdiff = 4; //pCodecCtx->qcompress = 0.6; pCodecCtx->qmin = 10; pCodecCtx->qmax = 51; //Optional Param pCodecCtx->max_b_frames = 3; // Set Option AVDictionary *param = 0; //H.264 if (pCodecCtx->codec_id == AV_CODEC_ID_H264) { av_opt_set(pCodecCtx->priv_data, "preset", "superfast", 0);// av_dict_set(&param, "tune", "animation", 0); av_dict_set(&param, "profile", "baseline", 0); } //Show some Information av_dump_format(pFormatCtx, 0, out_file, 1); pCodec = avcodec_find_encoder(pCodecCtx->codec_id); if (!pCodec) { LOGE(JNI_DEBUG, "Can not find encoder! \n"); return -1; } if (avcodec_open2(pCodecCtx, pCodec, &param) < 0) { LOGE(JNI_DEBUG, "Failed to open encoder! \n"); return -1; } pFrame = av_frame_alloc(); picture_size = avpicture_get_size(pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height); LOGI(JNI_DEBUG, " picture_size:%d", picture_size); uint8_t *buf = (uint8_t *) av_malloc(picture_size); avpicture_fill((AVPicture *) pFrame, buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height); //Write File Header avformat_write_header(pFormatCtx, NULL); av_new_packet(&pkt, picture_size); out_y_size = pCodecCtx->width * pCodecCtx->height; is_end = START_STATE; pthread_t thread; pthread_create(&thread, NULL, JXYUVEncodeH264::startEncode, this); LOGI(JNI_DEBUG, "视频编码器初始化完成") return 0;}/** * 发送一帧到编码队列 * @param buf * @return */int JXYUVEncodeH264::startSendOneFrame(uint8_t *buf) { int in_y_size = arguments->in_width * arguments->in_height; uint8_t *new_buf = (uint8_t *) malloc(in_y_size * 3 / 2); memcpy(new_buf, buf, in_y_size * 3 / 2); frame_queue.push(new_buf); return 0;}/** * 启动编码线程 * @param obj * @return */void *JXYUVEncodeH264::startEncode(void *obj) { JXYUVEncodeH264 *h264_encoder = (JXYUVEncodeH264 *) obj; while (!h264_encoder->is_end||!h264_encoder->frame_queue.empty()) { if (h264_encoder->frame_queue.empty()) { continue; } uint8_t *picture_buf = *h264_encoder->frame_queue.wait_and_pop().get(); LOGI(JNI_DEBUG, "send_videoframe_count:%d", h264_encoder->frame_count); int in_y_size = h264_encoder->arguments->in_width * h264_encoder->arguments->in_height; h264_encoder->custom_filter(h264_encoder, picture_buf, in_y_size, h264_encoder->arguments->v_custom_format);// h264_encoder->pFrame->data[0] = picture_buf;// h264_encoder->pFrame->data[2] = picture_buf + h264_encoder->out_y_size;// h264_encoder->pFrame->data[1] = picture_buf + h264_encoder->out_y_size * 5 / 4;// memcpy(h264_encoder->pFrame->data[0],picture_buf,h264_encoder->out_y_size);// memcpy(h264_encoder->pFrame->data[2],picture_buf+h264_encoder->out_y_size,h264_encoder->out_y_size/4);// memcpy(h264_encoder->pFrame->data[1],picture_buf+h264_encoder->out_y_size*5/4,h264_encoder->out_y_size/4); //PTS h264_encoder->pFrame->pts = h264_encoder->frame_count; h264_encoder->frame_count++; int got_picture = 0; //Encode int ret = avcodec_encode_video2(h264_encoder->pCodecCtx, &h264_encoder->pkt, h264_encoder->pFrame, &got_picture); if (ret < 0) { LOGE(JNI_DEBUG, "Failed to encode! \n"); } if (got_picture == 1) { LOGI(JNI_DEBUG, "Succeed to encode frame: %5d\tsize:%5d\n", h264_encoder->framecnt, h264_encoder->pkt.size); h264_encoder->framecnt++; h264_encoder->pkt.stream_index = h264_encoder->video_st->index; ret = av_write_frame(h264_encoder->pFormatCtx, &h264_encoder->pkt); av_free_packet(&h264_encoder->pkt); } delete (picture_buf); } if (h264_encoder->is_end) { h264_encoder->encodeEnd(); delete h264_encoder; } return 0;}/** * 对视频做一些处理 * @param h264_encoder * @param picture_buf * @param in_y_size * @param format */voidJXYUVEncodeH264::custom_filter(const JXYUVEncodeH264 *h264_encoder, const uint8_t *picture_buf, int in_y_size, int format) { // y值在H方向开始行 int y_height_start_index=h264_encoder->arguments->in_height-h264_encoder->arguments->out_height; // uv值在H方向开始行 int uv_height_start_index=y_height_start_index/2; if (format == ROTATE_90_CROP_LT) { for (int i = y_height_start_index; i < h264_encoder->arguments->in_height; i++) { for (int j = 0; j < h264_encoder->arguments->out_width; j++) { int index = h264_encoder->arguments->in_width * i + j; uint8_t value = *(picture_buf + index); *(h264_encoder->pFrame->data[0] + j * h264_encoder->arguments->out_height + (h264_encoder->arguments->out_height - (i-y_height_start_index) - 1)) = value; } } for (int i = uv_height_start_index; i < h264_encoder->arguments->in_height / 2; i++) { for (int j = 0; j < h264_encoder->arguments->out_width / 2; j++) { int index = h264_encoder->arguments->in_width / 2 * i + j; uint8_t v = *(picture_buf + in_y_size + index); uint8_t u = *(picture_buf + in_y_size * 5 / 4 + index); *(h264_encoder->pFrame->data[2] + (j * h264_encoder->arguments->out_height / 2 + (h264_encoder->arguments->out_height / 2 - (i-uv_height_start_index) - 1))) = v; *(h264_encoder->pFrame->data[1] + (j * h264_encoder->arguments->out_height / 2 + (h264_encoder->arguments->out_height / 2 - (i-uv_height_start_index) - 1))) = u; } } } else if (format == ROTATE_0_CROP_LT) { for (int i = y_height_start_index; i < h264_encoder->arguments->in_height; i++) { for (int j = 0; j < h264_encoder->arguments->out_width; j++) { int index = h264_encoder->arguments->in_width * i + j; uint8_t value = *(picture_buf + index); *(h264_encoder->pFrame->data[0] + (i-y_height_start_index) * h264_encoder->arguments->out_width + j) = value; } } for (int i = uv_height_start_index; i < h264_encoder->arguments->in_height / 2; i++) { for (int j = 0; j < h264_encoder->arguments->out_width / 2; j++) { int index = h264_encoder->arguments->in_width / 2 * i + j; uint8_t v = *(picture_buf + in_y_size + index); uint8_t u = *(picture_buf + in_y_size * 5 / 4 + index); *(h264_encoder->pFrame->data[2] + ((i-uv_height_start_index) * h264_encoder->arguments->out_width / 2 + j)) = v; *(h264_encoder->pFrame->data[1] + ((i-uv_height_start_index) * h264_encoder->arguments->out_width / 2 + j)) = u; } } } else if (format == ROTATE_270_CROP_LT_MIRROR_LR) { int y_width_start_index=h264_encoder->arguments->in_width-h264_encoder->arguments->out_width; int uv_width_start_index=y_width_start_index/2; for (int i = 0; i < h264_encoder->arguments->out_height; i++) { for (int j = y_width_start_index; j < h264_encoder->arguments->in_width; j++) { int index = h264_encoder->arguments->in_width * (h264_encoder->arguments->out_height-i-1) + j; uint8_t value = *(picture_buf + index); *(h264_encoder->pFrame->data[0] + (h264_encoder->arguments->out_width - (j-y_width_start_index) - 1) * h264_encoder->arguments->out_height + i) = value; } } for (int i = 0; i < h264_encoder->arguments->out_height / 2; i++) { for (int j = uv_width_start_index; j < h264_encoder->arguments->in_width / 2; j++) { int index = h264_encoder->arguments->in_width / 2 * (h264_encoder->arguments->out_height/2-i-1) + j; uint8_t v = *(picture_buf + in_y_size + index); uint8_t u = *(picture_buf + in_y_size * 5 / 4 + index); *(h264_encoder->pFrame->data[2] + (h264_encoder->arguments->out_width / 2 - (j-uv_width_start_index) - 1) * h264_encoder->arguments->out_height / 2 + i) = v; *(h264_encoder->pFrame->data[1] + (h264_encoder->arguments->out_width / 2 - (j-uv_width_start_index) - 1) * h264_encoder->arguments->out_height / 2 + i) = u; } } }}/** * 视频编码结束 * @return */int JXYUVEncodeH264::encodeEnd() { //Flush Encoder int ret_1 = flush_encoder(pFormatCtx, 0); if (ret_1 < 0) { LOGE(JNI_DEBUG, "Flushing encoder failed\n"); return -1; } //Write file trailer av_write_trailer(pFormatCtx); //Clean if (video_st) { avcodec_close(video_st->codec); av_free(pFrame);// av_free(picture_buf); } avio_close(pFormatCtx->pb); avformat_free_context(pFormatCtx); LOGI(JNI_DEBUG, "视频编码结束") arguments->handler->setup_video_state(END_STATE); arguments->handler->try_encode_over(arguments); return 1;}/** * 用户中断 */void JXYUVEncodeH264::user_end() { is_end = END_STATE;}

代码贴完了,现在来听本屌说说它的前世今生,很关键~。

1)视频编码器参数配置

这里稍微说几个重要的,一会没吐槽到的参数可以再开这里再仔细看看,。

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(&param, "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。

我们要对咋们采集的数据做处理,我们必须知道其数据类型和,在老版本的 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<480;i++){
//遍历高 for(int j=0;j<400;j++){
//遍历宽 int index=640*i+j;//当前遍历到的角标 unsigned char value=*(in_buf+index);//当前角标下的Y值// 开始赋值给我们的目标数组 *(out_buf_y+(i-(480-300))*400+j)=value;//目标数组是400*300的,这里是从0角标开始依次全部遍历且赋值 } }

假设in_buf是一帧YV12视频数据的话,执行完这个循环我们就得到剪切好的Y值了,接下来我们解析剪切UV数据,UV的模型和Y有点不同。之所以叫YUV4:2:0,不是因为没有V,它其实是在纵向上UV交换扫描的,比如第一行扫描U第二行就扫描V,第三行再扫描U。在横向上是隔一个扫描,比如第一列扫描了,第二列就不扫描,然后扫描第三列。所以U在横向和纵向上的数据都是其Y的1/2,总数量是其1/4,V也是一样的。知道了这些我们就可以轻易的建立模型。

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<480/2;i++){
//遍历高 for(int j=0;j<400/2;j++){
//遍历宽 int index=(640/2)*i+j;//当前遍历到的角标 unsigned char v=*(in_buf+(640*480)+index);//当前角标下的V值(指针位置得先向后移640*480个单位,因为前面放的是Y) unsigned char u=*(in_buf+(640*480*5/4)+index);//当前角标下的U值(指针位置得先向后移640*480*5/4个单位,因为前面放的是Y和V)// 从0角标开始赋值给我们的目标数组out_buf_u *(out_buf_u+(i-(480-300)/2)*400/2+j)=u; *(out_buf_v+(i-(480-300)/2)*400/2+j)=v; } }

经过上面的操作我们已经完成了最基本的剪切,摄像头采集的数据是横屏的,如果我们竖屏录制且我们不做任何操作的话这时候我们录制的视频是逆时针旋转了90°的,tnd你逆时针那哥就顺时针给你转90°,这样应该就正了。

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

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

unsigned char *in_buf;            unsigned char *out_buf_y;            for(int i=(480-300);i<480;i++){
//遍历高 for(int j=0;j<400;j++){
//遍历宽 int index=(640)*i+j;//当前遍历到的角标 unsigned char value=*(in_buf+index);//当前角标下的Y值 *(out_buf_y+j*300+(300-(i-(480-300)-1)))=value;//结合输出数组的图像即可明白 } }

Y弄好了UV就特别简单,因为我们已经掌握了规律,UV在横向和纵向上的值都是Y的一半。

剪切UV:

unsigned char *in_buf;            unsigned char *out_buf_u;            unsigned char *out_buf_v;            for(int i=(480-300)/2;i<480/2;i++){
//遍历高 for(int j=0;j<400/2;j++){
//遍历宽 int index=(640/2)*i+j;//当前遍历到的角标 unsigned char value_v=*(in_buf+(640*480)+index);//当前角标下的V值 unsigned char value_u=*(in_buf+(640*480*5/4)+index);//当前角标下的U值 *(out_buf_u+j*300/2+(300/2-(i-(480-300)/2-1)))=value_u;//结合输出数组的图像即可明白 *(out_buf_v+j*300/2+(300/2-(i-(480-300)/2-1)))=value_v;//结合输出数组的图像即可明白 } }

因为前置摄像头的原因,会导致镜像,所以在用前置摄像头录制的时候还需要处理镜像,更多详情查阅源码即可,除了这些我们可以做好多有趣的操作,比如当UV值都赋予128的时候就成了黑吧影像,你还可以调节亮度色调等等。

处理完数据后调用FFmpeg编码的API即可。

8.音频编码

从上面流程图看到其步骤也和视频差不多的,而且数据量比较小,用 libfdk-aac编的话基本能追上采集速度了,先上菜,再聊天:

jx_pcm_encode_aac.h:

/** * Created by jianxi on 2017/5/18. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#ifndef JIANXIFFMPEG_JX_PCM_ENCODE_AAC_H#define JIANXIFFMPEG_JX_PCM_ENCODE_AAC_H#include "base_include.h"#include "jx_user_arguments.h"using namespace std;/** * pcm编码为aac */class JXPCMEncodeAAC {public:    JXPCMEncodeAAC(UserArguments* arg);public:    int initAudioEncoder();    static void* startEncode(void* obj);    void user_end();    int sendOneFrame(uint8_t* buf);    int encodeEnd();private:    int flush_encoder(AVFormatContext *fmt_ctx, unsigned int stream_index);private:    threadsafe_queue
frame_queue; AVFormatContext *pFormatCtx; AVOutputFormat *fmt; AVStream *audio_st; AVCodecContext *pCodecCtx; AVCodec *pCodec; AVFrame *pFrame; AVPacket pkt; int got_frame = 0; int ret = 0; int size = 0; int i; int is_end=0; UserArguments *arguments; ~JXPCMEncodeAAC() { }};#endif //JIANXIFFMPEG_JX_PCM_ENCODE_AAC_H

jx_pcm_encode_aac.cpp:

/** * Created by jianxi on 2017/5/18. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#include "jx_pcm_encode_aac.h"#include "jx_jni_handler.h"#include "jx_log.h"#include 
JXPCMEncodeAAC::JXPCMEncodeAAC(UserArguments* arg):arguments(arg){}/** * 刷出编码器里剩余帧 * @param fmt_ctx * @param stream_index * @return */int JXPCMEncodeAAC::flush_encoder(AVFormatContext *fmt_ctx, unsigned int stream_index) { int ret; int got_frame; AVPacket enc_pkt; if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities & CODEC_CAP_DELAY)) return 0; while (1) { enc_pkt.data = NULL; enc_pkt.size = 0; av_init_packet(&enc_pkt); ret = avcodec_encode_audio2(fmt_ctx->streams[stream_index]->codec, &enc_pkt, NULL, &got_frame); av_frame_free(NULL); if (ret < 0) break; if (!got_frame) { ret = 0; break; } LOGI(JNI_DEBUG,"Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n", enc_pkt.size); /* mux encoded frame */ ret = av_write_frame(fmt_ctx, &enc_pkt); if (ret < 0) break; } return ret;}/** * 初始化音频编码器 * @return */int JXPCMEncodeAAC::initAudioEncoder() { LOGI(JNI_DEBUG,"音频编码器初始化开始") size_t path_length = strlen(arguments->audio_path); char *out_file=( char *)malloc(path_length+1); strcpy(out_file, arguments->audio_path); av_register_all(); //Method 1. pFormatCtx = avformat_alloc_context(); fmt = av_guess_format(NULL, out_file, NULL); pFormatCtx->oformat = fmt;// Method 2.// int a=avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, out_file);// fmt = pFormatCtx->oformat;// pCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);// 、 pCodecCtx = avcodec_alloc_context3(pCodec); //Open output URL if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0) { LOGE(JNI_DEBUG,"Failed to open output file!\n"); return -1; }// pFormatCtx->audio_codec_id=AV_CODEC_ID_AAC; audio_st = avformat_new_stream(pFormatCtx, 0); if (audio_st == NULL) { return -1; } pCodecCtx = audio_st->codec; pCodecCtx->codec_id = AV_CODEC_ID_AAC; pCodecCtx->codec_type = AVMEDIA_TYPE_AUDIO; pCodecCtx->sample_fmt = AV_SAMPLE_FMT_S16; pCodecCtx->sample_rate = arguments->audio_sample_rate; pCodecCtx->channel_layout = AV_CH_LAYOUT_MONO; pCodecCtx->channels = av_get_channel_layout_nb_channels(pCodecCtx->channel_layout); pCodecCtx->bit_rate = arguments->audio_bit_rate; pCodecCtx->thread_count = 2;// pCodecCtx->profile=FF_PROFILE_AAC_MAIN; int b= av_get_channel_layout_nb_channels(pCodecCtx->channel_layout); LOGI(JNI_DEBUG,"channels:%d",b); //Show some information av_dump_format(pFormatCtx, 0, out_file, 1); pCodec = avcodec_find_encoder(pCodecCtx->codec_id); if (!pCodec) { LOGE(JNI_DEBUG,"Can not find encoder!\n"); return -1; }// AVDictionary *param = 0;//// av_dict_set(&param, "profile", "aac_he", 0); int state = avcodec_open2(pCodecCtx, pCodec, NULL); if (state < 0) { LOGE(JNI_DEBUG,"Failed to open encoder!---%d",state); return -1; } pFrame = av_frame_alloc(); pFrame->nb_samples = pCodecCtx->frame_size; pFrame->format = pCodecCtx->sample_fmt; size = av_samples_get_buffer_size(NULL, pCodecCtx->channels, pCodecCtx->frame_size, pCodecCtx->sample_fmt, 1); uint8_t *frame_buf = (uint8_t *) av_malloc(size); avcodec_fill_audio_frame(pFrame, pCodecCtx->channels, pCodecCtx->sample_fmt, (const uint8_t *) frame_buf, size, 1); //Write Header avformat_write_header(pFormatCtx, NULL); av_new_packet(&pkt, size); is_end=START_STATE; pthread_t thread; pthread_create(&thread, NULL, JXPCMEncodeAAC::startEncode, this); LOGI(JNI_DEBUG,"音频编码器初始化完成") return 0;}/** * 用户结束标记 */void JXPCMEncodeAAC::user_end(){ is_end=END_STATE;}/** * 发送一帧到编码队列 * @param buf * @return */int JXPCMEncodeAAC::sendOneFrame(uint8_t* buf){ uint8_t *new_buf = (uint8_t *) malloc(size); memcpy(new_buf,buf,size); frame_queue.push(new_buf); return 0;}/** * 编码结束操作 * @return */int JXPCMEncodeAAC::encodeEnd(){ //Flush Encoder ret = flush_encoder(pFormatCtx, 0); if (ret < 0) { LOGE(JNI_DEBUG,"Flushing encoder failed\n"); return -1; } //Write Trailer av_write_trailer(pFormatCtx); //Clean if (audio_st) { avcodec_close(audio_st->codec); av_free(pFrame);// av_free(frame_buf); } avio_close(pFormatCtx->pb); avformat_free_context(pFormatCtx); LOGI(JNI_DEBUG,"音频编码完成") arguments->handler->setup_audio_state(END_STATE); arguments->handler->try_encode_over(arguments); return 0;}/** * 开启编码线程 * @param obj * @return */ void * JXPCMEncodeAAC::startEncode(void* obj) { JXPCMEncodeAAC *aac_encoder = (JXPCMEncodeAAC *)obj; while (!aac_encoder->is_end||!aac_encoder->frame_queue.empty()) { if(aac_encoder->frame_queue.empty()){ continue; } uint8_t *frame_buf = *aac_encoder->frame_queue.wait_and_pop().get(); aac_encoder->pFrame->data[0]=frame_buf; aac_encoder->pFrame->pts = aac_encoder->i ; aac_encoder->i++; aac_encoder->got_frame = 0; //Encode aac_encoder->ret = avcodec_encode_audio2(aac_encoder->pCodecCtx, &aac_encoder->pkt, aac_encoder->pFrame, &aac_encoder->got_frame); if (aac_encoder->ret < 0) { LOGE(JNI_DEBUG,"Failed to encode!\n"); } if (aac_encoder->got_frame == 1) { LOGI(JNI_DEBUG,"Succeed to encode 1 frame! \tsize:%5d\n", aac_encoder->pkt.size); aac_encoder->pkt.stream_index = aac_encoder->audio_st->index; aac_encoder-> ret = av_write_frame(aac_encoder->pFormatCtx, &aac_encoder->pkt); av_free_packet(&aac_encoder->pkt); } delete(frame_buf); } if (aac_encoder->is_end) { aac_encoder->encodeEnd(); delete aac_encoder; } return 0;}

音频我研究不是那么多,下面只简单介绍下参数,更多可访问

编码参数:

  • pCodecCtx->sample_fmt = AV_SAMPLE_FMT_S16 设定其采样格式,我们的为16位无符号整数,这里需要和Java音频采集的时候设置的参数对应。
  • pCodecCtx->sample_rate = arguments->audio_sample_rate 采样率,音频不是我们最重要的,这里我写死了主流的44100,这里也需要和Java音频采集的时候设置的参数对应。
  • pCodecCtx->channel_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了。

9. 编写视频合成类

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

jx_media_muxer.h:

/** * Created by jianxi on 2017/5/24. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#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

jx_media_muxer.cpp:

/** * Created by jianxi on 2017/5/24. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#include "jx_media_muxer.h"extern "C"{#include "jx_ffmpeg_cmd_run.h"}#include "jx_log.h"int JXMediaMuxer::startMuxer( const char *in_filename_v, const char *in_filename_a,const char *out_filename) {    size_t in_filename_v_size = strlen(in_filename_v);    char *new_in_filename_v = (char *) malloc(in_filename_v_size + 1);    strcpy((new_in_filename_v), in_filename_v);    size_t in_filename_a_size = strlen(in_filename_a);    char *new_in_filename_a = (char *) malloc(in_filename_a_size + 1);    strcpy((new_in_filename_a), in_filename_a);    size_t out_filename_size = strlen(out_filename);    char *new_out_filename = (char *) malloc(out_filename_size + 1);    strcpy((new_out_filename), out_filename);    LOGI(JNI_DEBUG, "视音编码成功,开始合成")    char *cmd[10];    cmd[0] = "ffmpeg";    cmd[1] = "-i";    cmd[2] = new_in_filename_v;    cmd[3] = "-i";    cmd[4] = new_in_filename_a;    cmd[5] = "-c:v";    cmd[6] = "copy";    cmd[7] = "-c:a";    cmd[8] = "copy";    cmd[9] = new_out_filename;    return ffmpeg_cmd_run(10, cmd);}

我靠,写到这提示太长叫别篇写~~我嘞个去,好吧,更多内容在下一篇,,在最后将分享一些学习方法与经验~~

请尊重原创,转载请注明出处:

预热


时光荏苒,光阴如梭,离上一次吹牛逼已经过去了两三个月,身边很多人的女票已经分了又合,合了又分,本屌依旧骄傲单身。上一次啊我们大致说了一些简单的FFmpeg命令以及层简单的调用方式,然后有很多朋友在github或者csdn上给我留言,很多时候我都选择避而不答,原因是本库以前用的so包是不开源的,我根本改不了里面东西。但是这一次啊我们玩点大的,我重新编译了FFmpeg且重写JNI的接口函数,这次将从CJava全面开源,2.0项目花了本尊两个多月的业余时间,今天终于完工,非常鸡冻,且本博客将抒发出作者的全部心声,有没有很鸡冻,有没有。鸡冻之余,我也想吐槽下其软便的效率,确实不是很高,3.0的时候将会试试硬编码,或则在2.0迭代的时候会采用H265编码,这都是后话了,不过看把小视频换成大视频的节奏,应该可以搞。

本文涉及知识点:
  • Andorid 视频和音频采集
  • YUV视频处理(手动剪切、旋转、镜像等)PCM音频处理
  • 利用FFmpeg API ,YUV编码为H264、PCM编码为AAC
  • FFmpeg 编码器的配置
  • JNI在工程中的实际运用
  • Android下FFmpeg命令工具的制作与应用
  • Android Studio插件 cMake 在工程中的应用
充能:
  • 至少需要知道YUV、PCM、MP4是什么()。
  • 最好能先阅读、、,为了不太啰嗦,这些文章中分享过的大多数知识将不再重复。
  • 对C/C++基本语法有基本的了解。
本人环境与工具
  • 系统: macOS-10.12.5
  • 编译器: Android Studio-2.3.2
  • ndk: r14
  • FFmpeg: 3.2.5

项目概括:

1. 效果图:

项目地址没变:
这里复用了1.0版本的gif图,因为界面一点没变,功能的话暂时没封装那么多,没关系后期会补上。

2. 整体流程:

3. 工程目录浏览:

新建项目


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

C++支持是必须的,至于选用C++ 11也是有原因的,后面我们会用的里面的一些API。
然后我们把在中编译好的六个动态库、头文件还有 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,这个文件暂时保留它。

编写JNI接口:

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

package com.mabeijianxi.smallvideorecord2.jniinterface;import java.util.ArrayList;/** * Created by jianxi on 2017/5/12. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */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,其内容暂时如下:

/** * Created by jianxi on 2017/5/12. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#include 
#include
using namespace std;/** * 编码准备,写入配置信息 */extern "C"JNIEXPORT jint JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_prepareJXFFmpegEncoder(JNIEnv *env, jclass type, jstring media_base_path_, jstring media_name_, jint v_custom_format, jint in_width, jint in_height, jint out_width, jint out_height, jint frame_rate, jlong video_bit_rate) {}/** * 编码一帧视频 */extern "C"JNIEXPORT jint JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_encodeFrame2H264(JNIEnv *env, jclass type, jbyteArray data_) { return 0;}/** * 获取ffmpeg编译信息 */extern "C"JNIEXPORT jstring JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_getFFmpegConfig(JNIEnv *env, jclass type) { return NULL;}/** * 编码一帧音频 */extern "C"JNIEXPORT jint JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_encodeFrame2AAC(JNIEnv *env, jclass type, jbyteArray data_) { return 0;}/** *结束 */extern "C"JNIEXPORT jint JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_recordEnd(JNIEnv *env, jclass type) { return 0;}JNIEXPORT void JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_nativeRelease(JNIEnv *env, jclass type) { // TODO}

编写native代码


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

1. 准备log函数:

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

jx_log.h:

/** * Created by jianxi on 2017/6/2. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#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 * mabeijianxi@gmail.com */#include "jx_log.h"int JNI_DEBUG= 1;

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

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

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

jx_ffmpeg_cmd_run.h:

/** * Created by jianxi on 2017/6/4. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#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 * mabeijianxi@gmail.com */#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. CMakeLists.txt编写:

先强上一个脚本:

# For more information about using CMake with Android Studio, read the# documentation: https://d.android.com/studio/projects/add-native-code.html# Sets the minimum version of CMake required to build the native library.cmake_minimum_required(VERSION 3.4.1)# Creates and names a library, sets it as either STATIC# or SHARED, and provides the relative paths to its source code.# You can define multiple libraries, and CMake builds them for you.# Gradle automatically packages shared libraries with your APK.add_library( # Sets the name of the library.             jx_ffmpeg_jni             # Sets the library as a shared library.             SHARED             # Provides a relative path to your source file(s).              src/main/cpp/jx_yuv_encode_h264.cpp              src/main/cpp/jx_pcm_encode_aac.cpp              src/main/cpp/jx_media_muxer.cpp              src/main/cpp/jx_jni_handler.cpp              src/main/cpp/jx_ffmpeg_jni.cpp              src/main/cpp/threadsafe_queue.cpp              src/main/cpp/jx_log.cpp              src/main/cpp/cmdutils.c              src/main/cpp/ffmpeg.c              src/main/cpp/ffmpeg_filter.c              src/main/cpp/ffmpeg_opt.c              src/main/cpp/jx_ffmpeg_cmd_run.c              src/main/cpp/jx_ffmpeg_config.cpp             )add_library(            avcodec            SHARED            IMPORTED            )set_target_properties(    avcodec    PROPERTIES IMPORTED_LOCATION    ${CMAKE_SOURCE_DIR}/src/main/cpp/libavcodec.so    )add_library(            avfilter            SHARED            IMPORTED             )set_target_properties(        avfilter        PROPERTIES IMPORTED_LOCATION        ${CMAKE_SOURCE_DIR}/src/main/cpp/libavfilter.so        )add_library(            avformat            SHARED            IMPORTED            )set_target_properties(            avformat            PROPERTIES IMPORTED_LOCATION            ${CMAKE_SOURCE_DIR}/src/main/cpp/libavformat.so            )add_library(            avutil            SHARED            IMPORTED            )set_target_properties(            avutil            PROPERTIES IMPORTED_LOCATION            ${CMAKE_SOURCE_DIR}/src/main/cpp/libavutil.so            )add_library(            swresample            SHARED            IMPORTED            )set_target_properties(            swresample            PROPERTIES IMPORTED_LOCATION            ${CMAKE_SOURCE_DIR}/src/main/cpp/libswresample.so             )add_library(            swscale            SHARED            IMPORTED            )set_target_properties(            swscale            PROPERTIES IMPORTED_LOCATION            ${CMAKE_SOURCE_DIR}/src/main/cpp/libswscale.so             )add_library(            jxffmpegcmd            SHARED            IMPORTED            )set_target_properties(            jxffmpegcmd            PROPERTIES IMPORTED_LOCATION            ${CMAKE_SOURCE_DIR}/src/main/cpp/libjxffmpegrun.so             )include_directories(    ${CMAKE_SOURCE_DIR}/ffmpeg-3.2.5/)# Searches for a specified prebuilt library and stores the path as a# variable. Because CMake includes system libraries in the search path by# default, you only need to specify the name of the public NDK library# you want to add. CMake verifies that the library exists before# completing its build.find_library( # Sets the name of the path variable.              log-lib              # Specifies the name of the NDK library that              # you want CMake to locate.              log )# Specifies libraries CMake should link to your target library. You# can link multiple libraries, such as libraries you define in this# build script, prebuilt third-party libraries, or system libraries.target_link_libraries( # Specifies the target library.                       jx_ffmpeg_jni                       avcodec                       avfilter                       avformat                       avutil                       swresample                       swscale                       # Links the target library to the log library                       # included in the NDK.                       ${log-lib} )

当然这个脚本是整个完整工程的,有些文件我们到后面才会建出来,现在就忍耐一下,如果你不想被爆红那么就需要每添加一个文件然后就在第一个 add_library 里面也添加一下,再点击 Studio的同步按钮。 里面其他 library 都是我们事先编译好copy进来的,所以采用预构建的方式添加,这里都是相对路径,所以你不需要修改什么。

include_directories 里面写上你已经编译过的源码的路径,很关键。这里面的头文件才是全的~。

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

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

threadsafe_queue.cpp

/** * Created by jianxi on 2017/5/31. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#ifndef JIANXIFFMPEG_THREADSAFE_QUEUE_CPP#define JIANXIFFMPEG_THREADSAFE_QUEUE_CPP#include 
#include
#include
#include
/** * 一个安全的队列 */template
class 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标准里面的啦~

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

其实这玩意和JavaBean差不多嘛,直接搞代码,代码中的JXJNIHandler

字段姑且当做没看到。

jx_user_arguments.h:

/** * Created by jianxi on 2017/5/26. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#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

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

6. 编写一个base.h

其实啊,当时写这个头文件是不想老去include同样的东西,我们视频编码与音频编码都需要要include的头文件放在了这里,并且定义了一些规则性的宏。

base_include.h:

/** * Created by jianxi on 2017/5/18. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#ifndef JIANXIFFMPEG_BASE_INCLUDE_H#define JIANXIFFMPEG_BASE_INCLUDE_Hextern "C"{#include "include/libavcodec/avcodec.h"#include "include/libavformat/avformat.h"#include "include/libavcodec/avcodec.h"#include "include/libavutil/opt.h"}#include "threadsafe_queue.cpp"#include 
#include
#define END_STATE 1#define START_STATE 0#define ROTATE_0_CROP_LT 0/** * 旋转90度剪裁左上 */#define ROTATE_90_CROP_LT 1/** * 暂时没处理 */#define ROTATE_180 2/** * 旋转270(-90)裁剪左上,左右镜像 */#define ROTATE_270_CROP_LT_MIRROR_LR 3using namespace std;#endif //JIANXIFFMPEG_BASE_INCLUDE_H

FFmpeg源码C的,include时 extern "C"很关键

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

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

有的兄弟可能会问为什么不编码一帧合成一帧,因为啊我了下合成时间,基本都是毫秒级别的,还有就是嫌麻烦,我这样做的话直接用我们制作的FFmpeg命令工具然后几行代码就搞定了,先上代码。

jx_yuv_encode_h264.h:

/** * Created by jianxi on 2017/5/12. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#ifndef JIANXIFFMPEG_JX_YUV_ENCODE_H264_H#define JIANXIFFMPEG_JX_YUV_ENCODE_H264_H#include "base_include.h"#include "jx_user_arguments.h"using namespace std;/** * yuv编码h264 */class JXYUVEncodeH264 {public:    JXYUVEncodeH264(UserArguments* arg);public:    int initVideoEncoder();    static void* startEncode(void * obj);    int startSendOneFrame(uint8_t *buf);    void user_end();    int encodeEnd();    void custom_filter(const JXYUVEncodeH264 *h264_encoder, const uint8_t *picture_buf,                       int in_y_size,                       int format);private:    int flush_encoder(AVFormatContext *fmt_ctx, unsigned int stream_index);private:    UserArguments *arguments;    int is_end = 0;    threadsafe_queue
frame_queue; AVFormatContext *pFormatCtx; AVOutputFormat *fmt; AVStream *video_st; AVCodecContext *pCodecCtx; AVCodec *pCodec; AVPacket pkt; AVFrame *pFrame; int picture_size; int out_y_size; int framecnt = 0; int frame_count = 0; ~JXYUVEncodeH264() { }};#endif //JIANXIFFMPEG_JX_YUV_ENCODE_H264_H

jx_yuv_encode_h264.cpp:

/** * Created by jianxi on 2017/5/12. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#include "jx_yuv_encode_h264.h"#include "jx_jni_handler.h"#include "jx_log.h"#include 
JXYUVEncodeH264::JXYUVEncodeH264(UserArguments *arg) : arguments(arg) {}/** * 结束编码时刷出还在编码器里面的帧 * @param fmt_ctx * @param stream_index * @return */int JXYUVEncodeH264::flush_encoder(AVFormatContext *fmt_ctx, unsigned int stream_index) { int ret; int got_frame; AVPacket enc_pkt; if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities & CODEC_CAP_DELAY)) return 0; while (1) { enc_pkt.data = NULL; enc_pkt.size = 0; av_init_packet(&enc_pkt); ret = avcodec_encode_video2(fmt_ctx->streams[stream_index]->codec, &enc_pkt, NULL, &got_frame); av_frame_free(NULL); if (ret < 0) break; if (!got_frame) { ret = 0; break; } LOGI(JNI_DEBUG, "_Flush Encoder: Succeed to encode 1 frame video!\tsize:%5d\n", enc_pkt.size); /* mux encoded frame */ ret = av_write_frame(fmt_ctx, &enc_pkt); if (ret < 0) break; } return ret;}/** * 初始化视频编码器 * @return */int JXYUVEncodeH264::initVideoEncoder() { LOGI(JNI_DEBUG, "视频编码器初始化开始") size_t path_length = strlen(arguments->video_path); char *out_file = (char *) malloc(path_length + 1); strcpy(out_file, arguments->video_path); av_register_all(); //Method1.// pFormatCtx = avformat_alloc_context();// //Guess Format// fmt = av_guess_format(NULL, out_file, NULL);//// LOGE(JNI_DEBUG,",fmt==null?:%s", fmt == NULL ? "null" : "no_null");// pFormatCtx->oformat = fmt; //Method 2. avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, out_file); fmt = pFormatCtx->oformat; //Open output URL if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0) { LOGE(JNI_DEBUG, "_Failed to open output file! \n"); return -1; } video_st = avformat_new_stream(pFormatCtx, 0); //video_st->time_base.num = 1; //video_st->time_base.den = 25; if (video_st == NULL) { LOGE(JNI_DEBUG, "_video_st==null"); return -1; } //Param that must set pCodecCtx = video_st->codec; //pCodecCtx->codec_id =AV_CODEC_ID_HEVC; pCodecCtx->codec_id = AV_CODEC_ID_H264; pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO; pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P; if (arguments->v_custom_format == ROTATE_0_CROP_LT || arguments->v_custom_format == ROTATE_180) { pCodecCtx->width = arguments->out_width; pCodecCtx->height = arguments->out_height; } else { pCodecCtx->width = arguments->out_height; pCodecCtx->height = arguments->out_width; } pCodecCtx->bit_rate = arguments->video_bit_rate; pCodecCtx->gop_size = 250; pCodecCtx->thread_count = 16; pCodecCtx->time_base.num = 1; pCodecCtx->time_base.den = arguments->frame_rate;// pCodecCtx->me_pre_cmp = 1; //H264 //pCodecCtx->me_range = 16; //pCodecCtx->max_qdiff = 4; //pCodecCtx->qcompress = 0.6; pCodecCtx->qmin = 10; pCodecCtx->qmax = 51; //Optional Param pCodecCtx->max_b_frames = 3; // Set Option AVDictionary *param = 0; //H.264 if (pCodecCtx->codec_id == AV_CODEC_ID_H264) { av_opt_set(pCodecCtx->priv_data, "preset", "superfast", 0);// av_dict_set(&param, "tune", "animation", 0); av_dict_set(&param, "profile", "baseline", 0); } //Show some Information av_dump_format(pFormatCtx, 0, out_file, 1); pCodec = avcodec_find_encoder(pCodecCtx->codec_id); if (!pCodec) { LOGE(JNI_DEBUG, "Can not find encoder! \n"); return -1; } if (avcodec_open2(pCodecCtx, pCodec, &param) < 0) { LOGE(JNI_DEBUG, "Failed to open encoder! \n"); return -1; } pFrame = av_frame_alloc(); picture_size = avpicture_get_size(pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height); LOGI(JNI_DEBUG, " picture_size:%d", picture_size); uint8_t *buf = (uint8_t *) av_malloc(picture_size); avpicture_fill((AVPicture *) pFrame, buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height); //Write File Header avformat_write_header(pFormatCtx, NULL); av_new_packet(&pkt, picture_size); out_y_size = pCodecCtx->width * pCodecCtx->height; is_end = START_STATE; pthread_t thread; pthread_create(&thread, NULL, JXYUVEncodeH264::startEncode, this); LOGI(JNI_DEBUG, "视频编码器初始化完成") return 0;}/** * 发送一帧到编码队列 * @param buf * @return */int JXYUVEncodeH264::startSendOneFrame(uint8_t *buf) { int in_y_size = arguments->in_width * arguments->in_height; uint8_t *new_buf = (uint8_t *) malloc(in_y_size * 3 / 2); memcpy(new_buf, buf, in_y_size * 3 / 2); frame_queue.push(new_buf); return 0;}/** * 启动编码线程 * @param obj * @return */void *JXYUVEncodeH264::startEncode(void *obj) { JXYUVEncodeH264 *h264_encoder = (JXYUVEncodeH264 *) obj; while (!h264_encoder->is_end||!h264_encoder->frame_queue.empty()) { if (h264_encoder->frame_queue.empty()) { continue; } uint8_t *picture_buf = *h264_encoder->frame_queue.wait_and_pop().get(); LOGI(JNI_DEBUG, "send_videoframe_count:%d", h264_encoder->frame_count); int in_y_size = h264_encoder->arguments->in_width * h264_encoder->arguments->in_height; h264_encoder->custom_filter(h264_encoder, picture_buf, in_y_size, h264_encoder->arguments->v_custom_format);// h264_encoder->pFrame->data[0] = picture_buf;// h264_encoder->pFrame->data[2] = picture_buf + h264_encoder->out_y_size;// h264_encoder->pFrame->data[1] = picture_buf + h264_encoder->out_y_size * 5 / 4;// memcpy(h264_encoder->pFrame->data[0],picture_buf,h264_encoder->out_y_size);// memcpy(h264_encoder->pFrame->data[2],picture_buf+h264_encoder->out_y_size,h264_encoder->out_y_size/4);// memcpy(h264_encoder->pFrame->data[1],picture_buf+h264_encoder->out_y_size*5/4,h264_encoder->out_y_size/4); //PTS h264_encoder->pFrame->pts = h264_encoder->frame_count; h264_encoder->frame_count++; int got_picture = 0; //Encode int ret = avcodec_encode_video2(h264_encoder->pCodecCtx, &h264_encoder->pkt, h264_encoder->pFrame, &got_picture); if (ret < 0) { LOGE(JNI_DEBUG, "Failed to encode! \n"); } if (got_picture == 1) { LOGI(JNI_DEBUG, "Succeed to encode frame: %5d\tsize:%5d\n", h264_encoder->framecnt, h264_encoder->pkt.size); h264_encoder->framecnt++; h264_encoder->pkt.stream_index = h264_encoder->video_st->index; ret = av_write_frame(h264_encoder->pFormatCtx, &h264_encoder->pkt); av_free_packet(&h264_encoder->pkt); } delete (picture_buf); } if (h264_encoder->is_end) { h264_encoder->encodeEnd(); delete h264_encoder; } return 0;}/** * 对视频做一些处理 * @param h264_encoder * @param picture_buf * @param in_y_size * @param format */voidJXYUVEncodeH264::custom_filter(const JXYUVEncodeH264 *h264_encoder, const uint8_t *picture_buf, int in_y_size, int format) { // y值在H方向开始行 int y_height_start_index=h264_encoder->arguments->in_height-h264_encoder->arguments->out_height; // uv值在H方向开始行 int uv_height_start_index=y_height_start_index/2; if (format == ROTATE_90_CROP_LT) { for (int i = y_height_start_index; i < h264_encoder->arguments->in_height; i++) { for (int j = 0; j < h264_encoder->arguments->out_width; j++) { int index = h264_encoder->arguments->in_width * i + j; uint8_t value = *(picture_buf + index); *(h264_encoder->pFrame->data[0] + j * h264_encoder->arguments->out_height + (h264_encoder->arguments->out_height - (i-y_height_start_index) - 1)) = value; } } for (int i = uv_height_start_index; i < h264_encoder->arguments->in_height / 2; i++) { for (int j = 0; j < h264_encoder->arguments->out_width / 2; j++) { int index = h264_encoder->arguments->in_width / 2 * i + j; uint8_t v = *(picture_buf + in_y_size + index); uint8_t u = *(picture_buf + in_y_size * 5 / 4 + index); *(h264_encoder->pFrame->data[2] + (j * h264_encoder->arguments->out_height / 2 + (h264_encoder->arguments->out_height / 2 - (i-uv_height_start_index) - 1))) = v; *(h264_encoder->pFrame->data[1] + (j * h264_encoder->arguments->out_height / 2 + (h264_encoder->arguments->out_height / 2 - (i-uv_height_start_index) - 1))) = u; } } } else if (format == ROTATE_0_CROP_LT) { for (int i = y_height_start_index; i < h264_encoder->arguments->in_height; i++) { for (int j = 0; j < h264_encoder->arguments->out_width; j++) { int index = h264_encoder->arguments->in_width * i + j; uint8_t value = *(picture_buf + index); *(h264_encoder->pFrame->data[0] + (i-y_height_start_index) * h264_encoder->arguments->out_width + j) = value; } } for (int i = uv_height_start_index; i < h264_encoder->arguments->in_height / 2; i++) { for (int j = 0; j < h264_encoder->arguments->out_width / 2; j++) { int index = h264_encoder->arguments->in_width / 2 * i + j; uint8_t v = *(picture_buf + in_y_size + index); uint8_t u = *(picture_buf + in_y_size * 5 / 4 + index); *(h264_encoder->pFrame->data[2] + ((i-uv_height_start_index) * h264_encoder->arguments->out_width / 2 + j)) = v; *(h264_encoder->pFrame->data[1] + ((i-uv_height_start_index) * h264_encoder->arguments->out_width / 2 + j)) = u; } } } else if (format == ROTATE_270_CROP_LT_MIRROR_LR) { int y_width_start_index=h264_encoder->arguments->in_width-h264_encoder->arguments->out_width; int uv_width_start_index=y_width_start_index/2; for (int i = 0; i < h264_encoder->arguments->out_height; i++) { for (int j = y_width_start_index; j < h264_encoder->arguments->in_width; j++) { int index = h264_encoder->arguments->in_width * (h264_encoder->arguments->out_height-i-1) + j; uint8_t value = *(picture_buf + index); *(h264_encoder->pFrame->data[0] + (h264_encoder->arguments->out_width - (j-y_width_start_index) - 1) * h264_encoder->arguments->out_height + i) = value; } } for (int i = 0; i < h264_encoder->arguments->out_height / 2; i++) { for (int j = uv_width_start_index; j < h264_encoder->arguments->in_width / 2; j++) { int index = h264_encoder->arguments->in_width / 2 * (h264_encoder->arguments->out_height/2-i-1) + j; uint8_t v = *(picture_buf + in_y_size + index); uint8_t u = *(picture_buf + in_y_size * 5 / 4 + index); *(h264_encoder->pFrame->data[2] + (h264_encoder->arguments->out_width / 2 - (j-uv_width_start_index) - 1) * h264_encoder->arguments->out_height / 2 + i) = v; *(h264_encoder->pFrame->data[1] + (h264_encoder->arguments->out_width / 2 - (j-uv_width_start_index) - 1) * h264_encoder->arguments->out_height / 2 + i) = u; } } }}/** * 视频编码结束 * @return */int JXYUVEncodeH264::encodeEnd() { //Flush Encoder int ret_1 = flush_encoder(pFormatCtx, 0); if (ret_1 < 0) { LOGE(JNI_DEBUG, "Flushing encoder failed\n"); return -1; } //Write file trailer av_write_trailer(pFormatCtx); //Clean if (video_st) { avcodec_close(video_st->codec); av_free(pFrame);// av_free(picture_buf); } avio_close(pFormatCtx->pb); avformat_free_context(pFormatCtx); LOGI(JNI_DEBUG, "视频编码结束") arguments->handler->setup_video_state(END_STATE); arguments->handler->try_encode_over(arguments); return 1;}/** * 用户中断 */void JXYUVEncodeH264::user_end() { is_end = END_STATE;}

代码贴完了,现在来听本屌说说它的前世今生,很关键~。

1)视频编码器参数配置

这里稍微说几个重要的,一会没吐槽到的参数可以再开这里再仔细看看,。

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(&param, "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。

我们要对咋们采集的数据做处理,我们必须知道其数据类型和,在老版本的 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<480;i++){
//遍历高 for(int j=0;j<400;j++){
//遍历宽 int index=640*i+j;//当前遍历到的角标 unsigned char value=*(in_buf+index);//当前角标下的Y值// 开始赋值给我们的目标数组 *(out_buf_y+(i-(480-300))*400+j)=value;//目标数组是400*300的,这里是从0角标开始依次全部遍历且赋值 } }

假设in_buf是一帧YV12视频数据的话,执行完这个循环我们就得到剪切好的Y值了,接下来我们解析剪切UV数据,UV的模型和Y有点不同。之所以叫YUV4:2:0,不是因为没有V,它其实是在纵向上UV交换扫描的,比如第一行扫描U第二行就扫描V,第三行再扫描U。在横向上是隔一个扫描,比如第一列扫描了,第二列就不扫描,然后扫描第三列。所以U在横向和纵向上的数据都是其Y的1/2,总数量是其1/4,V也是一样的。知道了这些我们就可以轻易的建立模型。

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<480/2;i++){
//遍历高 for(int j=0;j<400/2;j++){
//遍历宽 int index=(640/2)*i+j;//当前遍历到的角标 unsigned char v=*(in_buf+(640*480)+index);//当前角标下的V值(指针位置得先向后移640*480个单位,因为前面放的是Y) unsigned char u=*(in_buf+(640*480*5/4)+index);//当前角标下的U值(指针位置得先向后移640*480*5/4个单位,因为前面放的是Y和V)// 从0角标开始赋值给我们的目标数组out_buf_u *(out_buf_u+(i-(480-300)/2)*400/2+j)=u; *(out_buf_v+(i-(480-300)/2)*400/2+j)=v; } }

经过上面的操作我们已经完成了最基本的剪切,摄像头采集的数据是横屏的,如果我们竖屏录制且我们不做任何操作的话这时候我们录制的视频是逆时针旋转了90°的,tnd你逆时针那哥就顺时针给你转90°,这样应该就正了。

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

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

unsigned char *in_buf;            unsigned char *out_buf_y;            for(int i=(480-300);i<480;i++){
//遍历高 for(int j=0;j<400;j++){
//遍历宽 int index=(640)*i+j;//当前遍历到的角标 unsigned char value=*(in_buf+index);//当前角标下的Y值 *(out_buf_y+j*300+(300-(i-(480-300)-1)))=value;//结合输出数组的图像即可明白 } }

Y弄好了UV就特别简单,因为我们已经掌握了规律,UV在横向和纵向上的值都是Y的一半。

剪切UV:

unsigned char *in_buf;            unsigned char *out_buf_u;            unsigned char *out_buf_v;            for(int i=(480-300)/2;i<480/2;i++){
//遍历高 for(int j=0;j<400/2;j++){
//遍历宽 int index=(640/2)*i+j;//当前遍历到的角标 unsigned char value_v=*(in_buf+(640*480)+index);//当前角标下的V值 unsigned char value_u=*(in_buf+(640*480*5/4)+index);//当前角标下的U值 *(out_buf_u+j*300/2+(300/2-(i-(480-300)/2-1)))=value_u;//结合输出数组的图像即可明白 *(out_buf_v+j*300/2+(300/2-(i-(480-300)/2-1)))=value_v;//结合输出数组的图像即可明白 } }

因为前置摄像头的原因,会导致镜像,所以在用前置摄像头录制的时候还需要处理镜像,更多详情查阅源码即可,除了这些我们可以做好多有趣的操作,比如当UV值都赋予128的时候就成了黑吧影像,你还可以调节亮度色调等等。

处理完数据后调用FFmpeg编码的API即可。

8.音频编码

从上面流程图看到其步骤也和视频差不多的,而且数据量比较小,用 libfdk-aac编的话基本能追上采集速度了,先上菜,再聊天:

jx_pcm_encode_aac.h:

/** * Created by jianxi on 2017/5/18. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#ifndef JIANXIFFMPEG_JX_PCM_ENCODE_AAC_H#define JIANXIFFMPEG_JX_PCM_ENCODE_AAC_H#include "base_include.h"#include "jx_user_arguments.h"using namespace std;/** * pcm编码为aac */class JXPCMEncodeAAC {public:    JXPCMEncodeAAC(UserArguments* arg);public:    int initAudioEncoder();    static void* startEncode(void* obj);    void user_end();    int sendOneFrame(uint8_t* buf);    int encodeEnd();private:    int flush_encoder(AVFormatContext *fmt_ctx, unsigned int stream_index);private:    threadsafe_queue
frame_queue; AVFormatContext *pFormatCtx; AVOutputFormat *fmt; AVStream *audio_st; AVCodecContext *pCodecCtx; AVCodec *pCodec; AVFrame *pFrame; AVPacket pkt; int got_frame = 0; int ret = 0; int size = 0; int i; int is_end=0; UserArguments *arguments; ~JXPCMEncodeAAC() { }};#endif //JIANXIFFMPEG_JX_PCM_ENCODE_AAC_H

jx_pcm_encode_aac.cpp:

/** * Created by jianxi on 2017/5/18. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#include "jx_pcm_encode_aac.h"#include "jx_jni_handler.h"#include "jx_log.h"#include 
JXPCMEncodeAAC::JXPCMEncodeAAC(UserArguments* arg):arguments(arg){}/** * 刷出编码器里剩余帧 * @param fmt_ctx * @param stream_index * @return */int JXPCMEncodeAAC::flush_encoder(AVFormatContext *fmt_ctx, unsigned int stream_index) { int ret; int got_frame; AVPacket enc_pkt; if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities & CODEC_CAP_DELAY)) return 0; while (1) { enc_pkt.data = NULL; enc_pkt.size = 0; av_init_packet(&enc_pkt); ret = avcodec_encode_audio2(fmt_ctx->streams[stream_index]->codec, &enc_pkt, NULL, &got_frame); av_frame_free(NULL); if (ret < 0) break; if (!got_frame) { ret = 0; break; } LOGI(JNI_DEBUG,"Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n", enc_pkt.size); /* mux encoded frame */ ret = av_write_frame(fmt_ctx, &enc_pkt); if (ret < 0) break; } return ret;}/** * 初始化音频编码器 * @return */int JXPCMEncodeAAC::initAudioEncoder() { LOGI(JNI_DEBUG,"音频编码器初始化开始") size_t path_length = strlen(arguments->audio_path); char *out_file=( char *)malloc(path_length+1); strcpy(out_file, arguments->audio_path); av_register_all(); //Method 1. pFormatCtx = avformat_alloc_context(); fmt = av_guess_format(NULL, out_file, NULL); pFormatCtx->oformat = fmt;// Method 2.// int a=avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, out_file);// fmt = pFormatCtx->oformat;// pCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);// 、 pCodecCtx = avcodec_alloc_context3(pCodec); //Open output URL if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0) { LOGE(JNI_DEBUG,"Failed to open output file!\n"); return -1; }// pFormatCtx->audio_codec_id=AV_CODEC_ID_AAC; audio_st = avformat_new_stream(pFormatCtx, 0); if (audio_st == NULL) { return -1; } pCodecCtx = audio_st->codec; pCodecCtx->codec_id = AV_CODEC_ID_AAC; pCodecCtx->codec_type = AVMEDIA_TYPE_AUDIO; pCodecCtx->sample_fmt = AV_SAMPLE_FMT_S16; pCodecCtx->sample_rate = arguments->audio_sample_rate; pCodecCtx->channel_layout = AV_CH_LAYOUT_MONO; pCodecCtx->channels = av_get_channel_layout_nb_channels(pCodecCtx->channel_layout); pCodecCtx->bit_rate = arguments->audio_bit_rate; pCodecCtx->thread_count = 2;// pCodecCtx->profile=FF_PROFILE_AAC_MAIN; int b= av_get_channel_layout_nb_channels(pCodecCtx->channel_layout); LOGI(JNI_DEBUG,"channels:%d",b); //Show some information av_dump_format(pFormatCtx, 0, out_file, 1); pCodec = avcodec_find_encoder(pCodecCtx->codec_id); if (!pCodec) { LOGE(JNI_DEBUG,"Can not find encoder!\n"); return -1; }// AVDictionary *param = 0;//// av_dict_set(&param, "profile", "aac_he", 0); int state = avcodec_open2(pCodecCtx, pCodec, NULL); if (state < 0) { LOGE(JNI_DEBUG,"Failed to open encoder!---%d",state); return -1; } pFrame = av_frame_alloc(); pFrame->nb_samples = pCodecCtx->frame_size; pFrame->format = pCodecCtx->sample_fmt; size = av_samples_get_buffer_size(NULL, pCodecCtx->channels, pCodecCtx->frame_size, pCodecCtx->sample_fmt, 1); uint8_t *frame_buf = (uint8_t *) av_malloc(size); avcodec_fill_audio_frame(pFrame, pCodecCtx->channels, pCodecCtx->sample_fmt, (const uint8_t *) frame_buf, size, 1); //Write Header avformat_write_header(pFormatCtx, NULL); av_new_packet(&pkt, size); is_end=START_STATE; pthread_t thread; pthread_create(&thread, NULL, JXPCMEncodeAAC::startEncode, this); LOGI(JNI_DEBUG,"音频编码器初始化完成") return 0;}/** * 用户结束标记 */void JXPCMEncodeAAC::user_end(){ is_end=END_STATE;}/** * 发送一帧到编码队列 * @param buf * @return */int JXPCMEncodeAAC::sendOneFrame(uint8_t* buf){ uint8_t *new_buf = (uint8_t *) malloc(size); memcpy(new_buf,buf,size); frame_queue.push(new_buf); return 0;}/** * 编码结束操作 * @return */int JXPCMEncodeAAC::encodeEnd(){ //Flush Encoder ret = flush_encoder(pFormatCtx, 0); if (ret < 0) { LOGE(JNI_DEBUG,"Flushing encoder failed\n"); return -1; } //Write Trailer av_write_trailer(pFormatCtx); //Clean if (audio_st) { avcodec_close(audio_st->codec); av_free(pFrame);// av_free(frame_buf); } avio_close(pFormatCtx->pb); avformat_free_context(pFormatCtx); LOGI(JNI_DEBUG,"音频编码完成") arguments->handler->setup_audio_state(END_STATE); arguments->handler->try_encode_over(arguments); return 0;}/** * 开启编码线程 * @param obj * @return */ void * JXPCMEncodeAAC::startEncode(void* obj) { JXPCMEncodeAAC *aac_encoder = (JXPCMEncodeAAC *)obj; while (!aac_encoder->is_end||!aac_encoder->frame_queue.empty()) { if(aac_encoder->frame_queue.empty()){ continue; } uint8_t *frame_buf = *aac_encoder->frame_queue.wait_and_pop().get(); aac_encoder->pFrame->data[0]=frame_buf; aac_encoder->pFrame->pts = aac_encoder->i ; aac_encoder->i++; aac_encoder->got_frame = 0; //Encode aac_encoder->ret = avcodec_encode_audio2(aac_encoder->pCodecCtx, &aac_encoder->pkt, aac_encoder->pFrame, &aac_encoder->got_frame); if (aac_encoder->ret < 0) { LOGE(JNI_DEBUG,"Failed to encode!\n"); } if (aac_encoder->got_frame == 1) { LOGI(JNI_DEBUG,"Succeed to encode 1 frame! \tsize:%5d\n", aac_encoder->pkt.size); aac_encoder->pkt.stream_index = aac_encoder->audio_st->index; aac_encoder-> ret = av_write_frame(aac_encoder->pFormatCtx, &aac_encoder->pkt); av_free_packet(&aac_encoder->pkt); } delete(frame_buf); } if (aac_encoder->is_end) { aac_encoder->encodeEnd(); delete aac_encoder; } return 0;}

音频我研究不是那么多,下面只简单介绍下参数,更多可访问

编码参数:

  • pCodecCtx->sample_fmt = AV_SAMPLE_FMT_S16 设定其采样格式,我们的为16位无符号整数,这里需要和Java音频采集的时候设置的参数对应。
  • pCodecCtx->sample_rate = arguments->audio_sample_rate 采样率,音频不是我们最重要的,这里我写死了主流的44100,这里也需要和Java音频采集的时候设置的参数对应。
  • pCodecCtx->channel_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了。

9. 编写视频合成类

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

jx_media_muxer.h:

/** * Created by jianxi on 2017/5/24. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#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

jx_media_muxer.cpp:

/** * Created by jianxi on 2017/5/24. * https://github.com/mabeijianxi * mabeijianxi@gmail.com */#include "jx_media_muxer.h"extern "C"{#include "jx_ffmpeg_cmd_run.h"}#include "jx_log.h"int JXMediaMuxer::startMuxer( const char *in_filename_v, const char *in_filename_a,const char *out_filename) {    size_t in_filename_v_size = strlen(in_filename_v);    char *new_in_filename_v = (char *) malloc(in_filename_v_size + 1);    strcpy((new_in_filename_v), in_filename_v);    size_t in_filename_a_size = strlen(in_filename_a);    char *new_in_filename_a = (char *) malloc(in_filename_a_size + 1);    strcpy((new_in_filename_a), in_filename_a);    size_t out_filename_size = strlen(out_filename);    char *new_out_filename = (char *) malloc(out_filename_size + 1);    strcpy((new_out_filename), out_filename);    LOGI(JNI_DEBUG, "视音编码成功,开始合成")    char *cmd[10];    cmd[0] = "ffmpeg";    cmd[1] = "-i";    cmd[2] = new_in_filename_v;    cmd[3] = "-i";    cmd[4] = new_in_filename_a;    cmd[5] = "-c:v";    cmd[6] = "copy";    cmd[7] = "-c:a";    cmd[8] = "copy";    cmd[9] = new_out_filename;    return ffmpeg_cmd_run(10, cmd);}

我靠,写到这提示太长叫别篇写~~我嘞个去,好吧,更多内容在下一篇,在最后将分享一些学习方法与经验~~

你可能感兴趣的文章
覆盖equals方法时总是要覆盖hashCode
查看>>
clone详解
查看>>
【Java并发编程实战】——AbstractQueuedSynchronizer源码分析(一)
查看>>
【Java并发编程实战】——并发编程基础
查看>>
【Java并发编程实战】——Java内存模型与线程
查看>>
Java复制文件的4种方式
查看>>
mysql的JDBC连接工具类
查看>>
利用多线程(用到原子类AtomicInteger)往数据库批量插入大量数据
查看>>
多个线程操作数组
查看>>
定长线程池的应用
查看>>
ArrayBlockingQueue的简单使用
查看>>
Git 常用命令总结(一)
查看>>
Git 常用命令总结(二)
查看>>
JAVA 并发——synchronized的分析
查看>>
Echarts——使用 dataset 管理数据
查看>>
DES 加解密工具类
查看>>
SpringBoot多模块项目实践(Multi-Module)
查看>>
第一篇: 服务的注册与发现Eureka(Greenwich版)
查看>>
第二篇: 服务消费者(rest+ribbon)(Greenwich版本)
查看>>
第三篇: 服务消费者(Feign)(Greenwich版本)
查看>>