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

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

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

  1. 超级简单的Android Studio jni 实现(无需命令行)
  2. Android Studio使用gradle-experimental构建NDK工程(无需Android.mk、Application.mk文件)
  3. 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 寻找到解决办法。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值