背景
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)
也就是直接將其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;
}
這裏簡單進行介紹,傳入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);
}
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;
}
總結
本文簡單介紹了Android ffmpeg解碼多路h264視頻並顯示的流程並附有完整示例,希望能夠給有相關需求並且還在探索的同學一個參考,也希望大家多多指正,一起進步。