对最近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视频的解码程序
// (Must match defines in person.walker.androidh264decoder.H264Decoder.java)
//short *tmp_pic=NULL;
***
视频解码,一开始由于项目紧张,人员也比较缺,这边没有懂视频的解码。所以当时就随便在网络上搜罗了一个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项目中的视频编解码的总结
- Android视频播放项目总结之 得到手机中的所有视频数据
- Android视频播放项目总结之 让别的程序能调用我的视频播放器
- Android视频播放项目总结之 适配器绑定视频信息列表显示到界面(适配器简单优化)
- android下播放器(软解码YUV数据)视频输出方法总结
- Android视频播放项目总结之 点击listview条目调用手机上其他的视频播放器播放
- Android视频播放项目总结之 代码分享
- Android视频播放项目总结之 使用第三方Vitamio库,开发万能播放器(一)
- 最近的android项目开发问题总结
- Android视频播放项目总结之 使用VideoView定义自己的视频播放器
- Android视频播放项目总结之 使用第三方Vitamio库,开发万能播放器(三)
- Android视频播放项目总结之 使用第三方Vitamio库,开发万能播放器(四)
- 大疆无人机 Android 开发总结——视频解码
- Android视频播放项目总结之 使用第三方Vitamio库,开发万能播放器(二)
- Android视频播放项目总结之 使用Android中的videoView自己定义,暂停,播放时间,总时间,进度等。
- Android视频播放项目总结之 把毫秒转化成时、分、秒的工具类(自己分装的)
- Android视频播放项目总结之 思路梳理
- Android IOS WebRTC 音视频开发总结(三二)-- WebRTC项目开发建议
- 最近的android项目开发问题总结
- 最近几年做软件项目的心得总结