您的位置:首页 > 移动开发 > Android开发

对最近Android项目中的视频编解码的总结

2016-09-13 17:26 260 查看
最近的项目是一个智能摄像头的项目,对于我们Android客户端来讲,肯定核心的是就是音视频编解码了。对于硬件端,也就是摄像头端,在我接手项目的时候,他们都已经订好了技术方案。往客户端(iOS,Android,PC)三端传输的数据是音视频分开的。音频通过G711u,视频通过H264。我们客户端这边需要做的就是解码H264拿到未压缩的视频帧,和转码G711u到PCM可以播放
***

视频解码,一开始由于项目紧张,人员也比较缺,这边没有懂视频的解码。所以当时就随便在网络上搜罗了一个H264的解码器(只有SO库),没想到的是竟然可以用。所以本着互联网思维(这里省略100个呵呵),我们就临时用了这个解码器,虽然效果不理想但是还算可以用。后来到了项目中期,我们开始自己学习写解码器。这里推荐三个搭建Android Stuido 搭建NDK开发环境的文章。

超级简单的Android Studio jni 实现(无需命令行)

Android Studio使用gradle-experimental构建NDK工程(无需Android.mk、Application.mk文件)

Android Studio NDk调试(基于gradle-experimental插件与LLDB)

下面是我的Gradle的配置,供大家参考

apply plugin: 'com.android.model.application'

model {
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"

defaultConfig.with {
applicationId "liufei.person.ffmpegproject"
minSdkVersion.apiLevel = 15
targetSdkVersion.apiLevel = 23
}
}
android.ndk {
moduleName = "MyLibrary"
ldLibs.addAll(['android', 'log'])
}

repositories {
MyLibrary(PrebuiltLibraries) {

ffmpeg {
binaries.withType(SharedLibraryBinary) {
sharedLibraryFile = file("src/main/jni/libffmpeg.so");
}
}
}
}
android.productFlavors {
create("arm") {
ndk.abiFilters.add("armeabi")
}
create("arm7") {
ndk.abiFilters.add("armeabi-v7a")
}
// To include all cpu architectures, leaves abiFilters emptycreate("all")
}
android.sources {
main {
jni {
exportedHeaders {
srcDir "src/main/jni/include"
}
dependencies{
library "ffmpeg"

}
}
}
}
android.buildTypes {
release {
minifyEnabled = false
proguardFiles.add(file('proguard-rules.txt'))
}
}

}

dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.2.1'
compile 'com.android.support:cardview-v7:23.+'
compile 'com.android.support:design:23.2.1'
}



在搭建好环境后,就是视频编解码的相关知识,这里推荐雷霄骅(leixiaohua1020)的专栏,里面对于音视频编码的讲解真的是面面俱到,并且例子很丰富。对我这样的音视频编解码的新手真的很有帮助。通过阅读和总结雷神的例子,结合我的实际需求,下面是我自己实现的H264视频的解码程序

unsigned int *rgb_2_pix = NULL;
unsigned int *r_2_pix = NULL;
unsigned int *g_2_pix = NULL;
unsigned int *b_2_pix = NULL;

typedef struct DecoderContext {
int color_format;
struct AVCodec *codec;
struct AVCodecContext *codec_ctx;
struct AVFrame *src_frame;
struct AVFrame *dst_frame;
struct SwsContext *convert_ctx;
int frame_ready;
long timeBase;
int frameIndex;
} DecoderContext;

static void set_ctx(JNIEnv *env, jobject thiz, void *ctx) {
jclass cls = (*env)->GetObjectClass(env, thiz);
//最后一个参数的含义表示这个属性的类型为 int
jfieldID fid = (*env)->GetFieldID(env, cls, "cdata", "I");
//为fid 设值为ctx...相当于给个映射 和java层,然后在get
(*env)->SetIntField(env, thiz, fid, (jint) ctx);
}
static long getCurrentTimeStamp(){
//得到的单位是 nanosecond 级别的,所以要除以1000
clock_t t = clock();
return t / 1000;
}
static void *get_ctx(JNIEnv *env, jobject thiz) {
jclass cls = (*env)->GetObjectClass(env, thiz);
jfieldID fid = (*env)->GetFieldID(env, cls, "cdata", "I");
return (void *) (*env)->GetIntField(env, thiz, fid);
}

static void av_log_callback(void *ptr, int level, const char *fmt, __va_list vl) {
static char line[1024] = {0};
vsnprintf(line, sizeof(line), fmt, vl);
D(line);
}

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
av_register_all();
av_log_set_callback(av_log_callback);
return JNI_VERSION_1_4;
}

JNIEXPORT void JNI_OnUnload(JavaVM *vm, void *reserved) {

}
/*
* thiz 是调用这个Jni接口函数的java object
*/
JNIEXPORT void JNICALL Java_person_walker_natives_H264Decoder_nativeInit(JNIEnv *env, jobject thiz,
jint color_format) {
D("get into init method");
DecoderContext *ctx = calloc(1, sizeof(DecoderContext));

D("Creating native H264 decoder context");

switch (color_format) {
case COLOR_FORMAT_YUV420:
ctx->color_format = PIX_FMT_YUV420P;
break;
case COLOR_FORMAT_RGB565LE:
ctx->color_format = PIX_FMT_RGB565LE;
break;
case COLOR_FORMAT_BGR32:
ctx->color_format = PIX_FMT_BGR32;
break;
}
ctx->codec = avcodec_find_decoder(CODEC_ID_H264);
ctx->codec_ctx = avcodec_alloc_context3(ctx->codec);
//ctx->codec_ctx->pix_fmt = PIX_FMT_YUV420P;
//ctx->codec_ctx->flags2 |= CODEC_FLAG2_CHUNKS;

ctx->src_frame = av_frame_alloc();
ctx->dst_frame = av_frame_alloc();
ctx->timeBase = getCurrentTimeStamp();
ctx->frameIndex = 0;
avcodec_open2(ctx->codec_ctx, ctx->codec, NULL);
//为这个对象的cdata设置数据
set_ctx(env, thiz, ctx);
}

JNIEXPORT void JNICALL Java_person_walker_natives_H264Decoder_nativeDestroy(JNIEnv *env,
jobject thiz) {
DecoderContext *ctx = get_ctx(env, thiz);

D("Destroying native H264 decoder context");

avcodec_close(ctx->codec_ctx);
avcodec_free_context(&(ctx->codec_ctx));
av_free(ctx->src_frame);
av_free(ctx->dst_frame);
free(ctx);
}

JNIEXPORT jint JNICALL Java_person_walker_natives_H264Decoder_consumeNalUnitsFromDirectBuffer(
JNIEnv *env, jobject thiz,
jbyteArray nal_units, jint num_bytes, jlong pkt_pts) {
DecoderContext *ctx = get_ctx(env, thiz);
void *buf = NULL;
if (nal_units == NULL) {
D("Received null buffer, sending empty packet to decoder");
}
else {
buf  = (*env) -> GetByteArrayElements(env,nal_units,0);
if (buf == NULL) {
/*  buf = (*env)->GetByteArrayElements(env, nal_units);
if (buf == NULL) {*/
D("Error getting direct buffer address");
(*env)->ReleaseByteArrayElements(env, nal_units, buf, 0);
return -1;
}
}
long pts = getCurrentTimeStamp() - ctx->timeBase;
//将buf,这里指的是传入进来的H264数据组合成packet 结构体对象
AVPacket packet = {
.data = (uint8_t *) buf,
.size = num_bytes,
};

int frameFinished = 0;
//将packet里面的数据decode解码到 ctx->指向的src_frame中去,AvCodecContext已经初始化 在init时候
//作用是解码一帧数据
int res = avcodec_decode_video2(ctx->codec_ctx, ctx->src_frame, &frameFinished, &packet);
//int res2 = avcodec_decode_video2(ctx->codec_ctx, ctx->src_frame, &frameFinished, &packet);
if (frameFinished)
ctx->frame_ready = 1;
D("goted frame is : %d", frameFinished);
(*env)->ReleaseByteArrayElements(env, nal_units, buf, 0);

return res;
}

JNIEXPORT jboolean JNICALL Java_person_walker_natives_H264Decoder_isFrameReady(JNIEnv *env,
jobject thiz) {
DecoderContext *ctx = get_ctx(env, thiz);
return ctx->frame_ready ? JNI_TRUE : JNI_FALSE;
}

JNIEXPORT jint JNICALL Java_person_walker_natives_H264Decoder_getWidth(JNIEnv *env, jobject thiz) {
DecoderContext *ctx = get_ctx(env, thiz);
return ctx->codec_ctx->width;
}

JNIEXPORT jint JNICALL Java_person_walker_natives_H264Decoder_getHeight(JNIEnv *env, jobject thiz) {
DecoderContext *ctx = get_ctx(env, thiz);
return ctx->codec_ctx->height;
}

JNIEXPORT jint JNICALL Java_person_walker_natives_H264Decoder_getOutputByteSize(JNIEnv *env,
jobject thiz) {
DecoderContext *ctx = get_ctx(env, thiz);
return avpicture_get_size(ctx->color_format, ctx->codec_ctx->width, ctx->codec_ctx->height);
}

JNIEXPORT jint JNICALL JNICALL Java_person_walker_natives_H264Decoder_decodeFrameToDirectBuffer
(JNIEnv *env, jobject thiz, jbyteArray out_buffer, jint out_len) {
DecoderContext *ctx = get_ctx(env, thiz);

/*  if (!ctx->frame_ready)
return -1;*/

void *out_buf = (*env)->GetByteArrayElements(env, out_buffer, 0);
if (out_buf == NULL) {
D("Error getting direct buffer address");
return -2;
}

// long out_buf_len = i

int pic_buf_size = avpicture_get_size(ctx->color_format, ctx->codec_ctx->width,
ctx->codec_ctx->height);

if (out_len


音频解码

其实我们工程里面并有涉及到音频解码的内容,更多是音频的转码和编码压缩。这里重点说一下音频的压缩编码。因为我们的项目里面确实是用到了G711u转码到PCM的内容,但是我们是利用的网络上开源的代码,这里就不详细说了。对于PCM编码.我们后来是放弃了FFMpeg利用的ANdroid 新推出的mediacodec API.下面上代码

Mediacodec 初始化

private void initMediaCodec() {
//查找机器所支持的编码器
try {
mAebi = new MediaCodec.BufferInfo();
presentationTimeUs = System.currentTimeMillis();
mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
MediaFormat aformat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 8000, 1);
aformat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
aformat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);
aformat.setInteger(MediaFormat.KEY_BIT_RATE, 24000);
mediaCodec.configure(aformat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaCodec.start();
} catch (IOException e) {
e.printStackTrace();
}
}`re>


编码过程

try {
ByteBuffer[] inBuffers = mediaCodec.getInputBuffers();
ByteBuffer[] outBuffers = mediaCodec.getOutputBuffers();

if (true) {
int inBufferIndex = 0;
inBufferIndex = mediaCodec.dequeueInputBuffer(-1);
//Log.i(TAG, String.format("try to dequeue input vbuffer, ii=%d", inBufferIndex));
if (inBufferIndex >= 0) {
//这个inBuffers是我们保存原始PCM的buffer数组
ByteBuffer bb = inBuffers[inBufferIndex];
bb.clear();
bb.put(bytes, 0, size);
long pts = System.currentTimeMillis() - presentationTimeUs;
mediaCodec.queueInputBuffer(inBufferIndex, 0, size, pts, 0);
}
}

while (true) {
int outBufferIndex = mediaCodec.dequeueOutputBuffer(mAebi, 0);
if (outBufferIndex >= 0) {
ByteBuffer bb = outBuffers[outBufferIndex];

int outPacketSize = mAebi.size + 7;//头部大小加buffer大小
bb.position(mAebi.offset);
bb.limit(mAebi.offset + mAebi.size);
byte[] outBytes = new byte[outPacketSize];
//对于PCM编码,一定不要忘记为AAC数据加上ADT头
addADTStoPacket(outBytes, outPacketSize);
bb.get(outBytes, 7, mAebi.size);
bb.position(mAebi.offset);
mPcmDatas.offer(outBytes, 100, TimeUnit.MILLISECONDS);
mediaCodec.releaseOutputBuffer(outBufferIndex, false);
} else {
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IllegalStateException e){
e.printStackTrace();
}


// (Must match defines in person.walker.androidh264decoder.H264Decoder.java)

//short *tmp_pic=NULL;

为编码好的AAC数据添加AAC头

/**
* AAC每个数据块钱都要添加ADTS头,不然播放不了
*
* @param packet
* @param packetLen
*/
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; // AAC LC
int freqIdx = 11; // 8KHz
int chanCfg = 1; // 单声道

// fill in ADTS data
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1)


总结

从项目一开始设计到音视频编码,到自学完成入门,再到应用到项目中,前前后后也花了不少时间。一开始以为音视频编解码是一个很是高大上工程,但是真正接触到才体会到,虽然音视频编解码确实是很高深,但是使用起来并不困难,因为前人已经为你走过了很多路,能让你快速掌握并且应用到项目中。即使遇到问题,也可以通过各种问答社区例如stackoverflow 寻找到解决办法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android 视频
相关文章推荐