视频学习笔记:Android ffmpeg解码多路h264视频并显示

背景

Android设备上使用ffmpeg解码多路h264视频,抽取了一个简单demo方便日后参考,在此记录一下。demo中主要涉及以下功能:

1.ffmpeg解码h264视频为yuv帧 
2.使用ffmpeg将yuv帧转换为可以在画布上渲染的rgb帧 
3.将android的SurfaceView类传入jni层并使用rgb帧进行渲染 
4.使用Java类包装c++类,多线程解码多路视频 
5.集成了OpenCV相关功能,在本例中可以使用相关api保存arg帧

其中解码部分代码参考了雷神博客的相关文章,并已经在项目中多处使用,surfaceview渲染部分参考了ijkplayer中的相关源码。项目地址如下,具体功能可以参考源码。

项目地址:https://git.oschina.net/vonchenchen/android_ffmpge_muti_decode.git

下面简单介绍一下工程的主要内容

功能实现

总体功能

首先需要使用ndk编译ffmpeg源码,这部分网上已经有比较多的介绍,在此不再赘述,编译好的文件已经在工程中可以直接使用。加载ffmpeg动态链接库的命令在Android.mk中,内容如下:

#ffmpeg
include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg
LOCAL_SRC_FILES := ../jniLibs/$(TARGET_ARCH_ABI)/libffmpeg.so
include $(PREBUILT_SHARED_LIBRARY)
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

也就是直接将其copy到jniLibs文件的对应目录即可。

decoder.cpp文件负责具体调用ffmpeg的功能进行解码。之前的项目中一直使用这个类,直接定义一个变量并调用相关解码方法。对于解码多路视频,如果直接在jni文件中定义多个对象则显得比较啰嗦,如果能使用java类包装一下decode类,需要一个decode类就new一个对应的java类,不需要时让java回收,这是比较理想的,对此专门学习了一下这种实现,原理见这里的另外一篇博客http://blog.csdn.net/lidec/article/details/72872037

当我们解码完毕后,需要把rgb帧渲染到画布上,这个过程参考了ijkplayer的实现,将surface作为一个参数传入jni层,拿到surface的缓冲区后将生成的rgb数据直接copy到这个缓冲区即可完成显示。相关ndk的api可以参考ndk文档,链接https://developer.android.com/ndk/reference/group___native_activity.html。这里注意,在编译时,LOCAL_LDLIBS中需要加入-ljnigraphics -landroid两个库。

解码与显示

这个类参考了雷神的博客,使用c++简单封装了一下。主干功能如下

int decoder::decodeFrame(const char *data, int length, void (*handle_data)(AVFrame *pFrame, void *param, void *ctx), void *ctx) {

    int cur_size = length;
    int ret = 0;

    memcpy(inbuf, data, length);
    const uint8_t *cur_ptr = inbuf;
    // Parse input stream to check if there is a valid frame.
    //std::cout << " in data  --  -- " << length<< std::endl;
    while(cur_size >0)
    {
        int parsedLength = av_parser_parse2(parser, codecContext, &avpkt.data,
                &avpkt.size, (const uint8_t*)cur_ptr , cur_size, AV_NOPTS_VALUE,
                AV_NOPTS_VALUE, AV_NOPTS_VALUE);
        cur_ptr += parsedLength;
        cur_size -= parsedLength;

        ...

        if (!avpkt.size) {
            continue;
        } else {

                int len, got_frame;
                len = avcodec_decode_video2(codecContext, frame, &got_frame,
                        &avpkt);

                if (len < 0) {
                   ...
                }

                if (got_frame) {
                    frame_count++;

                    LOGE("frame %d", frame_count);

                    if(img_convert_ctx == NULL){
                        img_convert_ctx = sws_getContext(codecContext->width, codecContext->height,
                                                         codecContext->pix_fmt, codecContext->width, codecContext->height,
                                                         pixelFormat, SWS_BICUBIC, NULL, NULL, NULL);

                        ...
                    }

                    if(img_convert_ctx != NULL) {
                        handle_data(frame, renderParam, ctx);
                    }
                }
        }
    }
    return length;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

这里简单进行介绍,传入h264视频buffer后利用av_parser_parse2解析出h264头在当前buffer中的偏移量,然后使用avcodec_decode_video2函数进行解码,最后得到一个AVFrame帧,这个帧就是h264流中的yuv帧。拿到这个帧和其他相关信息后,我们将这些内容传递给handle_data这个函数指针,由外面传入的handle_data函数处理生成的yuv帧。 
帧处理回调在com_vonchenchen_android_video_demos_codec_CodecWrapper.cpp中实现,这个函数主要完成将yuv数据使用ffmpeg转换为rgb帧,并且获取surface的缓冲区,将rgb拷贝到这段缓冲区中。具体实现如下:

void handle_data(AVFrame *pFrame, void *param, void *ctx){

    RenderParam *renderParam = (RenderParam *)param;

    //yuv420p转换为rgb565
    AVFrame *rgbFrame = yuv420p_2_argb(pFrame, renderParam->swsContext, renderParam->avCodecContext, pixelFormat);//AV_PIX_FMT_RGB565LE

    LOGE("width %d height %d",rgbFrame->width, rgbFrame->height);

    //for test decode image
    //save_rgb_image(rgbFrame);

    //用于传递上下文信息
    EnvPackage *envPackage = (EnvPackage *)ctx;

    //创建一个用来维护显示缓冲区的变量
    ANativeWindow_Buffer nwBuffer;
    //将java中的Surface对象转换为ANativeWindow指针
    ANativeWindow *aNativeWindow = ANativeWindow_fromSurface(envPackage->env, *(envPackage->surface));
    if (aNativeWindow == NULL) {
        LOGE("ANativeWindow_fromSurface error");
        return;
    }

    //用来缩放rgb帧与显示窗口的大小,使得rgb数据可以适应窗口大小
    int retval = ANativeWindow_setBuffersGeometry(aNativeWindow, rgbFrame->width, rgbFrame->height,  WINDOW_FORMAT_RGB_565);

    //锁定surface 
    if (0 != ANativeWindow_lock(aNativeWindow, &nwBuffer, 0)) {
        LOGE("ANativeWindow_lock error");
        return;
    }

    //将rgb数据拷贝给suface的缓冲区
    if (nwBuffer.format == WINDOW_FORMAT_RGB_565) {
        memcpy((__uint16_t *) nwBuffer.bits, (__uint16_t *)rgbFrame->data[0], rgbFrame->width * rgbFrame->height *2);
    }

    //解锁surface并显示新的缓冲区
    if(0 !=ANativeWindow_unlockAndPost(aNativeWindow)){
        LOGE("ANativeWindow_unlockAndPost error");
        return;
    }
    //清理垃圾
    ANativeWindow_release(aNativeWindow);

    //清理rgb帧结构以及帧结构所指向的rgb缓冲区
    av_free(rgbFrame->data[0]);
    av_free(rgbFrame);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

yuv转rgb

AVFrame *yuv420p_2_argb(AVFrame *frame, SwsContext *swsContext, AVCodecContext *avCodecContext, enum AVPixelFormat format){
    AVFrame *pFrameRGB = NULL;
    uint8_t  *out_bufferRGB = NULL;
    pFrameRGB = av_frame_alloc();

    pFrameRGB->width = frame->width;
    pFrameRGB->height = frame->height;

    //给pFrameRGB帧加上分配的内存;  //AV_PIX_FMT_ARGB
    int size = avpicture_get_size(format, avCodecContext->width, avCodecContext->height);
    //out_bufferRGB = new uint8_t[size];
    out_bufferRGB = av_malloc(size * sizeof(uint8_t));
    avpicture_fill((AVPicture *)pFrameRGB, out_bufferRGB, format, avCodecContext->width, avCodecContext->height);
    //YUV to RGB
    sws_scale(swsContext, frame->data, frame->linesize, 0, avCodecContext->height, pFrameRGB->data, pFrameRGB->linesize);

    return pFrameRGB;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

总结

本文简单介绍了Android ffmpeg解码多路h264视频并显示的流程并附有完整示例,希望能够给有相关需求并且还在探索的同学一个参考,也希望大家多多指正,一起进步。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值