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

版权声明:欢迎转载,转载请保留文章出处。 https://blog.csdn.net/junzia/article/details/68952183

前面博客记录了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。

猜你喜欢

转载自blog.csdn.net/junzia/article/details/68952183