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

来源:互联网 发布:淘宝集市店 编辑:程序博客网 时间:2024/06/06 09:33

背景

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中,内容如下:

#ffmpeginclude $(CLEAR_VARS)LOCAL_MODULE := ffmpegLOCAL_SRC_FILES := ../jniLibs/$(TARGET_ARCH_ABI)/libffmpeg.soinclude $(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视频并显示的流程并附有完整示例,希望能够给有相关需求并且还在探索的同学一个参考,也希望大家多多指正,一起进步。

原创粉丝点击