Android FFMpeg(三)——使用FFMpeg解碼h264、aac

前面博客記錄了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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章