前面博客記錄了FFMpeg的編譯,編譯後我們可以拿到FFMpeg的動態庫和靜態庫,拿到這些庫文件後,通常我們需要做個簡單的封裝才能在Android上層愉快的使用。本篇博客的是從拿到FFMpeg靜態庫到使用FFMpeg解碼視頻的過程,記錄儘可能的詳盡,可能會讓博客的篇幅略長。
準備工作
庫文件
本篇博客的示例是利用FFMPeg靜態庫進行解碼的,所以首先我們需要得到FFMpeg的靜態庫,編譯可以參照之前的兩篇博客。剛開始學習FFMpeg編解碼,直接用整個FFMpeg庫,不裁剪最好不過了,等熟悉後再裁剪掉不需要的功能。編譯之後,得到的靜態庫如下:
另外,博客項目使用的IDE是Android Studio 2.3,FFMpeg的封裝用的是C++,利用CMake構建編譯(可以和Java一樣,代碼提示和補全)。
gradle配置
在建立項目的時候會有選項,Link C++ project with gradle,勾選上,就會在module中生成CMakeLists.txt文件,並生成src/main/cpp/native-lib.cpp示例。如果要在以前的沒有使用CMake的項目中來使用FFMpeg,可以在module目錄下新建CMakeLists.txt,然後右鍵module,Link C++ project with gradle。當然也可以自己手動配置,最終gradle的配置如下:
module的gradle配置如下:
android {
compileSdkVersion 24
buildToolsVersion "24.0.2"
defaultConfig {
applicationId "edu.wuwang.ffmpeg"
minSdkVersion 15
targetSdkVersion 24
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
arguments '-DANDROID_PLATFORM=android-19','-DANDROID_TOOLCHAIN=clang', '-DANDROID_STL=gnustl_static'
cppFlags "-IE://Android/SDK/ndk-bundle/sources/cxx-stl/gnu-libstdc++/4.9/include",
"-I./include",'-D__STDC_CONSTANT_MACROS'
}
}
ndk{
abiFilters "armeabi-v7a"
}
}
sourceSets{
main{
jniLibs.srcDirs=['libs']
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
兩個externalNativeBuild是重點,前面一個是CMake的構建項目的一些參數,參數的具體設置見Android Developers官網,參數__STDC_CONSTANT_MACROS
是編譯的時候有錯,根據提示加上去的。後面一個是指定構建C++項目的配置文件。
CMakeLists.txt文件編寫
直接貼上文件內容了,詳細cmake語法,可以看下官方文檔:
# value of 3.4.0 or lower.
cmake_minimum_required(VERSION 2.8)
# ffmpeg靜態庫的路徑賦值給LIBDIR
set(LIBDIR ${CMAKE_CURRENT_SOURCE_DIR}/libs/armeabi-v7a)
# 設置cflag 使用c++11
set(CMAKE_C_FLAGS -std=c++11)
# 遍歷src/main/cpp下的source文件,賦值給DIR_SRCS
aux_source_directory(./src/main/cpp DIR_SRCS)
# ffmpeg的頭文件目錄
include_directories(./include)
# 編譯cpp下的資源爲名叫FFMpeg的動態庫
add_library(FFMpeg SHARED ${DIR_SRCS})
# 編譯FFMpeg動態庫需要用到的lib,注意依賴關係決定順序,被依賴的在後面
target_link_libraries(FFMpeg
${LIBDIR}/libavformat.a
${LIBDIR}/libavcodec.a
#libx264.a
${LIBDIR}/libavdevice.a
${LIBDIR}/libavfilter.a
${LIBDIR}/libavutil.a
${LIBDIR}/libswscale.a
${LIBDIR}/libswresample.a
${LIBDIR}/libpostproc.a
log z m)
檢查準備工作是否做好
準備工作做好後,創建FFMpeg.java類(後續會不斷修改):
public class FFMpeg {
static {
System.loadLibrary("FFMpeg");
}
public static native String getConfiguration();
public static native void init();
public native void setOutput(String path);
public native void start();
public native void stop();
public native void frame(byte[] frame);
public native void set(int key,int value);
public static native void release();
}
生成jni文件,實現getConfiguration方法和init方法:
jstring Java_edu_wuwang_ffmpeg_FFMpeg_getConfiguration(JNIEnv *env, jclass obj) {
return env->NewStringUTF(avcodec_configuration());
}
void Java_edu_wuwang_ffmpeg_FFMpeg_init(JNIEnv *env, jclass obj) {
av_register_all();
}
將內容打印出來,或者顯示到屏幕上。如果打印成功,準備工作就算是差不多了。當然調用下init方法,如果出現錯誤,錯誤指向Android系統庫的的一些方法,就需要檢查FFMpeg靜態庫是否編譯有問題,或者版本對不上之類的問題。
解碼過程
Log
如果完成了上述準備工作,能夠正常的打印出ffmpeg的配置信息,並且調用init方法也沒錯誤。就可以開始我們的FFMpeg的學習之旅了。
子曰:“工欲善其事,必先利其器。
爲了能夠方便的知道我們編寫的程序執行狀況,我們可以在安裝LLDB工具後,直接利用Android Studio來debug,進行單步調試。當然,更爲親和的方法,是在需要的地方,輸出Log到logcat界面。在CMakeLists.txt中,我們在 target_link_libraries的時候,已經加入了Android的log庫,在使用的時候,我們直接利用__android_log_print方法來輸出log。但是這並不是一個好的選擇。
在ffmpeg框架中,也有log模塊,在ffmpeg中使用最頻繁的函數之一就是:av_log。我們在使用ffmpeg進行編解碼的時候,應該儘可能使我們的使用ffmpeg的那部分代碼不依賴Android,以便於用到其他的地方。所以使用ffmpeg的log,比使用Android的log是更好的選擇。
ffmpeg的log模塊提供了av_log_set_callback方法,類似於java中的回調,我們可以在Jni文件中實現av_log_set_callback能接受的回調方法,在其中使用Android的log方法來打印日誌,這樣ffmpeg裏面的執行log我們就都可以在logcat上面看到了。
創建ffmpeg_log.h文件:
#ifndef AUDIOVIDEO_FFMPEG_LOG_H
#define AUDIOVIDEO_FFMPEG_LOG_H
#ifdef __cplusplus
extern "C"{
#endif
#include "androidlog.h"
#include "libavutil/log.h"
#define FF_LOG_TAG "FFMPEG_LOG_"
#define VLOG(level, TAG, ...) ((void)__android_log_vprint(level, TAG, __VA_ARGS__))
#define VLOGV(...) VLOG(ANDROID_LOG_VERBOSE, FF_LOG_TAG, __VA_ARGS__)
#define VLOGD(...) VLOG(ANDROID_LOG_DEBUG, FF_LOG_TAG, __VA_ARGS__)
#define VLOGI(...) VLOG(ANDROID_LOG_INFO, FF_LOG_TAG, __VA_ARGS__)
#define VLOGW(...) VLOG(ANDROID_LOG_WARN, FF_LOG_TAG, __VA_ARGS__)
#define VLOGE(...) VLOG(ANDROID_LOG_ERROR, FF_LOG_TAG, __VA_ARGS__)
#define ALOG(level, TAG, ...) ((void)__android_log_print(level, TAG, __VA_ARGS__))
#define ALOGV(...) ALOG(ANDROID_LOG_VERBOSE, FF_LOG_TAG, __VA_ARGS__)
#define ALOGD(...) ALOG(ANDROID_LOG_DEBUG, FF_LOG_TAG, __VA_ARGS__)
#define ALOGI(...) ALOG(ANDROID_LOG_INFO, FF_LOG_TAG, __VA_ARGS__)
#define ALOGW(...) ALOG(ANDROID_LOG_WARN, FF_LOG_TAG, __VA_ARGS__)
#define ALOGE(...) ALOG(ANDROID_LOG_ERROR, FF_LOG_TAG, __VA_ARGS__)
static void callback_report(void *ptr, int level, const char *fmt, va_list vl) {
int ffplv;
switch (level){
case AV_LOG_ERROR:
ffplv = ANDROID_LOG_ERROR;
break;
case AV_LOG_WARNING:
ffplv = ANDROID_LOG_WARN;
break;
case AV_LOG_INFO:
ffplv = ANDROID_LOG_INFO;
break;
case AV_LOG_VERBOSE:
ffplv=ANDROID_LOG_VERBOSE;
default:
ffplv = ANDROID_LOG_DEBUG;
break;
}
va_list vl2;
char line[1024];
static int print_prefix = 1;
va_copy(vl2, vl);
av_log_format_line(ptr, level, fmt, vl2, line, sizeof(line), &print_prefix);
va_end(vl2);
ALOG(ffplv, FF_LOG_TAG, "%s", line);
}
#ifdef __cplusplus
}
#endif
#endif //AUDIOVIDEO_FFMPEG_LOG_H
然後FFMpeg_jni.cpp的init方法中調用av_log_set_callback(callback_report)
,這樣我們就可以在Android Studio logcat界面看到ffmpeg的log了,在我們使用ffmpeg的過程中,也可以使用av_log來輸出log。
解碼流程
利用FFMpeg進行解碼的步驟如下,FFMPeg編解碼過程在雷神的博客中有兩張圖,很直觀。雷神的博客。
# 1-6爲第一階段;都是準備工作,7-9爲第二階段,解碼工作;10爲第三階段,收尾工作
# 1.調用此方法,註冊所有的編解碼器
av_register_all()
# 2.然後需要解碼的時候,調用此方法獲的一個AVFormatContext供後面過程使用
avformat_alloc_context()
# 3.打開需要解碼的音/視頻文件用來獲取相關信息
avformat_open_input()
# 4.讀取音/視頻流的相關信息
avformat_find_stream_info()
# 5.獲得解碼器
avcodec_find_decoder() or avcodec_find_decoder_by_name()
# 6.打開音/視頻文件用來解碼
avcodec_open2
# 7.讀取一個Package,讀取成功進入第8步,
av_read_frame()
# 8.對送入的數據進行解碼
avcodec_send_packet()
avcodec_receive_frame()
# 9.獲取解碼後的數據做相應的處理,進入第7部
# 10. 關閉解碼器及輸入
avcodec_close()
avformat_close_input()
解碼流程初步實踐(H264)
根據上面的解碼流程來做最簡單的實踐,我們知道我們看的視頻以Mp4爲例,是既有聲音又有圖像的。一般來說Mp4是H264或其變種的視頻與AAC的音頻混合成的。我們初步接觸使用FFMpeg,還是從簡入繁比較好,先單一的解碼H264文件。
第一步(初始化)
先按照第一階段1-6步,做準備工作:
void Decoder::start(){
avCodecID=AV_CODEC_ID_H264;
avFormatContext=avformat_alloc_context();
input= (char *) "/mnt/sdcard/test.264";
if(input==NULL){
av_log(NULL,AV_LOG_DEBUG,"input is null,please set input");
return;
}
int ret=avformat_open_input(&avFormatContext,input,NULL,NULL);
if(ret!=0){
av_log(NULL,AV_LOG_DEBUG,"avformat_open_input error:%d",ret);
return;
}
ret=avformat_find_stream_info(avFormatContext,NULL);
if(ret<0){
av_log(NULL,AV_LOG_DEBUG,"avformat_find_stream_info error:%d",ret);
return;
}
avCodec=avcodec_find_decoder(avCodecID);
avCodecContext=avcodec_alloc_context3(avCodec);
ret=avcodec_open2(avCodecContext,avCodec,NULL);
if(ret!=0){
av_log(NULL,AV_LOG_DEBUG,"avcodec_open2 error:%d",ret);
} else{
av_log(NULL,AV_LOG_DEBUG,"-----------------start success------------------");
avPacket=av_packet_alloc();
av_init_packet(avPacket);
avFrame=av_frame_alloc();
}
}
先使用固定的文件,跑通流程,直接將文件寫死在方法中了,後面再改爲由Java傳值進來。調用此方法,得到信息如下:
從中可以看到一行的內容爲:Reinit context to 384*288 pix_fmt:yuv420p
。
這就是H264文件視頻寬高爲384*288,色彩空間爲yuv420p,這個爲我們下一步工作做準備了。
第二步(解碼)
7-9步爲第二階段,解碼工作。初步實踐中,我們先把所有的流程跑通來,數據直接用準備工作得到的數據寫死在方法裏面,後面完善的時候再去修改。YUV420P的數據,是由Y/U/V三個分量組成,對於384*288的圖像,Y大小爲384*288字節,在AVFrame->data[0]中。U大小爲384*288/4字節,在AVFrame->data[1]中。V大小也爲384*288/4字節,在AVFrame->data[2]中。根據YUV的原理,我們可以將Y作爲R/G/B,顯示出來,將會得到一個與實際圖像基本一致的黑白圖像。so ,just do it。
//解碼一幀數據
int Decoder::frame(uint8_t * data) {
int ret=av_read_frame(avFormatContext,avPacket);
if(ret<0){
av_log(NULL,AV_LOG_DEBUG,"av_read_frame end:%d",ret);
return -1;
}
ret=avcodec_send_packet(avCodecContext,avPacket);
if(ret!=0){
av_log(NULL,AV_LOG_DEBUG,"avcodec_send_packet error:%d",ret);
return -2;
}
ret=avcodec_receive_frame(avCodecContext,avFrame);
if(ret==0){
//取得Y分量,傳遞出去
memcpy(data,avFrame->data[0], 384*288);
//UV分量
//memcpy(data+384*288,avFrame->data[1],384*288>>2);
//memcpy(data+384*288+384*288/4,avFrame->data[2],384*288>>2);
}
av_packet_unref(avPacket);
av_log(NULL,AV_LOG_DEBUG,"avFrame data[0] size:%d",sizeof(avFrame->data[0]));
//todo show to screen
return ret;
}
然後我們可以用ImageView或者OpenGL將Y分量顯示出來(OpenGL顯示Y分量,可在源碼中查看,或者我的關於OpenGL的筆記)。正確的話,得到如下的圖像:
接着把UV分量也傳遞出去,然後利用GPU將YUV轉換成RGB渲染出來,就可以得到彩色圖像了:
GPU YUV轉RGB的爲:
precision mediump float;
uniform sampler2D texY;
uniform sampler2D texU;
uniform sampler2D texV;
varying vec2 textureCoordinate;
void main(){
vec4 color = vec4((texture2D(texY, textureCoordinate).r - 16./255.) * 1.164);
vec4 U = vec4(texture2D(texU, textureCoordinate).r - 128./255.);
vec4 V = vec4(texture2D(texV, textureCoordinate).r - 128./255.);
color += V * vec4(1.596, -0.813, 0, 0);
color += U * vec4(0, -0.392, 2.017, 0);
color.a = 1.0;
gl_FragColor = color;
}
第三步(收尾工作)
第三步就沒什麼特殊的了,不再使用解碼的時候,把相關內容釋放掉就OK了:
void YDecoder::stop() {
//還有其他的一些XX也一起釋放掉
avcodec_close(avCodecContext);
avformat_close_input(&avFormatContext);
}
解碼AAC音頻實踐
上面我們解碼了H264,現在我們再嘗試下解碼AAC音頻文件。圖像的原始數據是YUV或者RGB,音頻的原始數據是PCM。我們解碼AAC,就是將AAC解碼爲PCM格式的數據,我們先將AAC解碼後的PCM數據保存起來,寫入文件,然後用第三方軟件播放,以確定我們解碼的數據是否正確。推薦一個工具Audacity,還挺好用的。
依舊是按照上面的解碼流程,三個步驟:
第一步(初始化)
方法的調用和上面解碼H264也基本一樣,增加了打開一個文件,用於保存解碼後的PCM數據。
int AACDecoder::start() {
const char * test="/mnt/sdcard/test.aac";
avFormatContext=avformat_alloc_context();
file=fopen("/mnt/sdcard/save.pcm","w+b");
int ret=avformat_open_input(&avFormatContext,test,NULL,NULL);
if(ret!=0){
log(ret,"avformat_open_input");
return ret;
}
ret=avformat_find_stream_info(avFormatContext,NULL);
if(ret<0){
log(ret,"avformat_find_stream_info");
return ret;
}
avCodec=avcodec_find_decoder(AV_CODEC_ID_AAC);
avCodecContext=avcodec_alloc_context3(avCodec);
ret=avcodec_open2(avCodecContext,avCodec,NULL);
if(ret!=0){
log(ret,"avcodec_open2");
return ret;
}
AVCodecParameters * param=avFormatContext->streams[0]->codecpar;
bitRate= (long) param->bit_rate;
sampleRate=param->sample_rate;
channelCount=param->channels;
audioFormat=param->format;
frameSize= (size_t) param->frame_size;
bytesPerSample = (size_t) av_get_bytes_per_sample(avCodecContext->sample_fmt);
avPacket=av_packet_alloc();
av_init_packet(avPacket);
avFrame=av_frame_alloc();
av_log(NULL,AV_LOG_DEBUG," start success,%d",bytesPerSample);
return 0;
}
執行後,得到的數據如下:
可以看出,解碼後,音頻數據採樣率爲44100,單通道,32位浮點型(8代表的類型AV_SAMPLE_FMT_FLTP,Android API21 後支持,FFMpeg貌似是2.1以後,用的基本都是這個類型)。
更好的方式,是利用Android Studio 的Debug功能,結合FFMpeg的頭文件,找我們需要的數據。因爲我們使用的只有一路AAC流,所以我們執行avformat_find_stream_info方法後,可以從我們使用的AVFormatContext示例中得到我們需要的數據,avFormatContext->streams[0]->codecpar:
上面的H264解碼,或者其他的解碼此方法也通用。
第二步(解碼)
int AACDecoder::output(uint8_t *data) {
int ret=av_read_frame(avFormatContext,avPacket);
if(ret!=0){
log(ret,"av_read_frame");
return ret;
}
ret=avcodec_send_packet(avCodecContext,avPacket);
if(ret!=0){
log(ret,"avcodec_send_packet");
return ret;
}
ret=avcodec_receive_frame(avCodecContext,avFrame);
bytesPerSample = (size_t) av_get_bytes_per_sample(avCodecContext->sample_fmt);
if(ret==0){
//PCM採樣數據的排列方式,一般是交錯排列輸出,AVFrame中存儲PCM數據各個通道是分開存儲的
//所以多通道的時候,需要根據PCM的格式和通道數,排列好後存儲
if(channelCount>1){
//多通道的
for (int i = 0; i < frameSize; i++) {
for (int j=0;j< channelCount;j++){
// memcpy(data+(i*channelCount+j)*bytesPerSample, avFrame->data[j]+i*bytesPerSample,bytesPerSample);
fwrite(avFrame->data[j]+i*bytesPerSample,1,bytesPerSample,file);
}
}
av_log(NULL,AV_LOG_DEBUG,"avcodec_receive_frame ok,%d,%d",bytesPerSample*frameSize*2,avFrame->nb_samples);
}else{
//單通道的,
// memcpy(data,avFrame->data[0],frameSize*bytesPerSample);
fwrite(avFrame->data[0],1,frameSize*bytesPerSample,file);
}
}else{
log(ret,"avcodec_receive_frame");
}
av_packet_unref(avPacket);
return ret;
}
第三步(收尾工作)
int AACDecoder::stop() {
fclose(file);
avcodec_free_context(&avCodecContext);
avformat_close_input(&avFormatContext);
return 0;
}
在Android中,通過如下的方式調用後,我們就可以在sdcard上得到一個save.pcm的文件,利用第三方的PCM播放工具,可以測試保存的PCM文件是否正確:
new Thread(new Runnable() {
@Override
public void run() {
mpeg.start();
while (!isDestoryed){
if(mpeg.output(tempData)==FFMpeg.EOF){
break;
}
}
mpeg.stop();
}
}).start();
然後,確定我們解碼後存儲的PCM文件是正確的後,對上面的代碼作簡單的修改,就可以直接利用Android的AudioTrack播放FFMpeg解碼AAC得到的PCM文件流了。
源碼示例
按照上面所述,我們已經成功的解碼了H264視頻文件和AAC音頻文件了,綜合H264的解碼和AAC的解碼我們就可以解碼Mp4文件,類似的其他的音視頻文件的解碼也就不再話下了,下一篇博客,將會補上Mp4文件的解碼。源碼在Github上,有需要的可以下載看看。H264解碼,參看H264Decoder.cpp。AAC解碼,參看AACDecoder.cpp。