利用FFmpeg玩轉Android視頻錄製與壓縮(二)<轉>

轉載出處:http://blog.csdn.net/mabeijianxi/article/details/72983362html

 

預熱


時光荏苒,光陰如梭,離上一次吹牛逼已通過去了兩三個月,身邊不少人的女票已經分了又合,合了又分,本屌依舊驕傲單身。上一次啊咱們大體說了一些簡單的FFmpeg命令以及Java層簡單的調用方式,而後有不少朋友在github或者csdn上給我留言,不少時候我都選擇避而不答,緣由是本庫之前用的so包是不開源的,我根本改不了裏面東西。可是這一次啊咱們玩點大的,我從新編譯了FFmpeg且重寫JNI的接口函數,此次將從C到Java全面開源,2.0項目花了本尊兩個多月的業餘時間,今天終於完工,很是雞凍,且本博客將抒發出做者的所有心聲,有沒有很雞凍,有沒有。雞凍之餘,我也想吐槽下其軟便的效率,確實不是很高,3.0的時候將會試試硬編碼,或則在2.0迭代的時候會採用H265編碼,這都是後話了,不過看微信把小視頻換成大視頻的節奏,應該能夠搞。java

本文涉及知識點:
  • Andorid 視頻和音頻採集
  • YUV視頻處理(手動剪切、旋轉、鏡像等)PCM音頻處理
  • 利用FFmpeg API ,YUV編碼爲H26四、PCM編碼爲AAC
  • FFmpeg 編碼器的配置
  • JNI在工程中的實際運用
  • Android下FFmpeg命令工具的製做與應用
  • Android Studio插件 cMake 在工程中的應用
充能:
本人環境與工具
  • 系統: macOS-10.12.5
  • 編譯器: Android Studio-2.3.2
  • ndk: r14
  • FFmpeg: 3.2.5

項目歸納:

1. 效果圖:


項目地址沒變:https://github.com/mabeijianxi/small-video-record
這裏複用了1.0版本的gif圖,由於界面一點沒變,功能的話暫時沒封裝那麼多,不要緊後期會補上。android

2. 總體流程:

3. 工程目錄瀏覽:

新建項目


咱們新建一個項目,也許與以往不一樣,須要勾選上 C++ 支持與 C++ standard選項時選擇 C++ 11,以下圖:c++


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目錄應該以下git


也許你會比我多一個自動生成的native-lib.cpp,這個文件暫時保留它。github

編寫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<FFmpegStateListener> 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 <jni.h> #include <string> using namespace std; /** * 編碼準備,寫入配置信息 */ extern "C" JNIEXPORT jint JNICALL Java_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 JNICALL Java_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_encodeFrame2H264(JNIEnv *env, jclass type, jbyteArray data_) { return 0; } /** * 獲取ffmpeg編譯信息 */ extern "C" JNIEXPORT jstring JNICALL Java_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_getFFmpegConfig(JNIEnv *env, jclass type) { return NULL; } /** * 編碼一幀音頻 */ extern "C" JNIEXPORT jint JNICALL Java_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_encodeFrame2AAC(JNIEnv *env, jclass type, jbyteArray data_) { return 0; } /** *結束 */ extern "C" JNIEXPORT jint JNICALL Java_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_recordEnd(JNIEnv *env, jclass type) { return 0; } JNIEXPORT void JNICALL Java_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_nativeRelease(JNIEnv *env, jclass type) { // TODO }

編寫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 * mabeijianxi@gmail.com */ #ifndef JIANXIFFMPEG_JX_LOG_H #define JIANXIFFMPEG_JX_LOG_H #include <android/log.h> 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接口:

這裏假設你已經看完了編譯Android下可執行命令的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 <jni.h> JNIEXPORT jint JNICALL Java_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 JNICALL Java_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 JNICALL Java_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 JNICALL Java_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 裏面也添加一下,再點擊Android 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 <queue> #include <memory> #include <mutex> #include <condition_variable> /** * 一個安全的隊列 */ template<typename T> class threadsafe_queue { private: mutable std::mutex mut; std::queue<T> data_queue; std::condition_variable data_cond; public: threadsafe_queue() {} threadsafe_queue(threadsafe_queue const &other) { std::lock_guard<std::mutex> lk(other.mut); data_queue = other.data_queue; } void push(T new_value)//入隊操做 { std::lock_guard<std::mutex> lk(mut); data_queue.push(new_value); data_cond.notify_one(); } void wait_and_pop(T &value)//直到有元素能夠刪除爲止 { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this] { return !data_queue.empty(); }); value = data_queue.front(); data_queue.pop(); } std::shared_ptr<T> wait_and_pop() { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this] { return !data_queue.empty(); }); std::shared_ptr<T> res(std::make_shared<T>(data_queue.front())); data_queue.pop(); return res; } bool try_pop(T &value)//無論有沒有隊首元素直接返回 { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) return false; value = data_queue.front(); data_queue.pop(); return true; } std::shared_ptr<T> try_pop() { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) return std::shared_ptr<T>(); std::shared_ptr<T> res(std::make_shared<T>(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_H extern "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 <jni.h> #include <string> #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 3 using 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<uint8_t *> 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 <pthread.h> 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 */ void JXYUVEncodeH264::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)視頻編碼器參數配置

這裏稍微說幾個重要的,一會沒吐槽到的參數能夠再開這裏再仔細看看,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(&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。
咱們要對咋們採集的數據作處理,咱們必須知道其數據類型和數據結構,在老版本的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<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<uint8_t *> 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 <pthread.h> 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; }

音頻我研究不是那麼多,下面只簡單介紹下參數,更多可訪問視音頻數據處理入門:PCM音頻採樣數據處理
編碼參數:

  • 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玩轉Android視頻錄製與壓縮(三),在最後將分享一些學習方法與經驗~~

相關文章
相關標籤/搜索