本文共 6633 字,大约阅读时间需要 22 分钟。
背景
设备上使用ffmpeg解码多路h264视频,抽取了一个简单demo方便日后参考,在此记录一下。demo中主要涉及以下功能:
1.ffmpeg解码h264视频为yuv帧
2.使用ffmpeg将yuv帧转换为可以在画布上渲染的rgb帧
3.将的SurfaceView类传入jni层并使用rgb帧进行渲染
4.使用类包装c++类,多线程解码多路视频
5.集成了相关功能,在本例中可以使用相关api保存arg帧
其中解码部分代码参考了雷神博客的相关文章,并已经在项目中多处使用,surfaceview渲染部分参考了ijkplayer中的相关源码。项目地址如下,具体功能可以参考源码。
项目地址:
下面简单介绍一下工程的主要内容
功能实现
总体功能
首先需要使用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回收,这是比较理想的,对此专门学习了一下这种实现,原理见这里的另外一篇博客。
当我们解码完毕后,需要把rgb帧渲染到画布上,这个过程参考了ijkplayer的实现,将surface作为一个参数传入jni层,拿到surface的缓冲区后将生成的rgb数据直接copy到这个缓冲区即可完成显示。相关ndk的api可以参考ndk文档,链接。这里注意,在编译时,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视频并显示的流程并附有完整示例,希望能够给有相关需求并且还在探索的同学一个参考,也希望大家多多指正,一起进步。
转载地址:http://bjzoi.baihongyu.com/