视频解码,一开始由于项目紧张,人员也比较缺,这边没有懂视频的解码。所以当时就随便在网络上搜罗了一个H264的解码器(只有SO库),没想到的是竟然可以用。所以本着互联网思维(这里省略100个呵呵),我们就临时用了这个解码器,虽然效果不理想但是还算可以用。后来到了项目中期,我们开始自己学习写解码器。这里推荐三个搭建Android Stuido 搭建NDK开发环境的文章。
下面是我的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.下面上代码
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每个数据块钱都要添加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 寻找到解决办法。