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