FFmpeg 像素格式转换(YUV转RGB)及使用SurfaceView显示

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/myvest/article/details/90717333

1、FFmpeg像素格式转换

FFmpeg像素转换一般使用libswscale来进行

接口说明

1、 获取上下文SwsContext
一般我们使用下面两个函数来获取,sws_getCachedContext和sws_getContext略有不同的是,如果输入和输出的宽/高/格式不变,则会返回之前创建的context。
参数说明:
前三个参数分变为原始宽、高、格式(如RGBA8888,YUV420等);后三个参数为转换后的宽、高、格式;参数flags可以指定转换时使用的算法;最后三个参数指定为NULL即可。

struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
                                  int dstW, int dstH, enum AVPixelFormat dstFormat,
                                  int flags, SwsFilter *srcFilter,
                                  SwsFilter *dstFilter, const double *param);
struct SwsContext *sws_getCachedContext(struct SwsContext *context,
                                        int srcW, int srcH, enum AVPixelFormat srcFormat,
                                        int dstW, int dstH, enum AVPixelFormat dstFormat,
                                        int flags, SwsFilter *srcFilter,
                                        SwsFilter *dstFilter, const double *param);

2、 转换函数sws_scale
参数说明:
参数struct SwsContext *c:上下文。
参数const uint8_t *const srcSlice[]:输入数据,为一个指针数据,指向每个通道的数据。一般可以从解码出来的frame->data作为该参数。
参数const int srcStride[]:每个通道行字节数。一般可以将解码出来的frame->linesize作为该参数。

stride定义下一行的起始位置。stride和width不一定相同,这是因为:
1)由于数据帧存储的对齐,有可能会向每行后面增加一些填充字节这样 stride = width + N;
2)packet色彩空间下,每个像素几个通道数据混合在一起,例如RGB24,每个像素3字节连续存放,因此下一行的位置需要跳过3*width字节。
srcSlice和srcStride的维数相同,由srcFormat值来。

csp 维数 宽width 跨度stride 高 YUV420 3 w,
w/2, w/2 s, s/2, s/2 h, h/2, h/2 YUYV 1 w, w/2, w/2
2s, 0, 0 h, h, h NV12 2 w, w/2, w/2 s, s, 0
h, h/2 RGB24 1 w, w, w 3s, 0, 0 h, 0, 0

参数int srcSliceY, int srcSliceH:定义在输入图像上处理区域,srcSliceY是起始位置,srcSliceH是处理多少行。如果srcSliceY=0,srcSliceH=height,表示一次性处理完整个图像。
这种设置是为了多线程并行,例如可以创建两个线程,第一个线程处理 [0, h/2-1]行,第二个线程处理 [h/2, h-1]行。并行处理加快速度。

参数uint8_t *const dst[], const int dstStride[]定义输出图像信息(输出的每个通道数据指针,每个通道行字节数)
这两个参数可以使用av_image_alloc进行获取,也可以根据输出的像素格式自行定义。

int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
              const int srcStride[], int srcSliceY, int srcSliceH,
              uint8_t *const dst[], const int dstStride[]);

3、释放函数

void sws_freeContext(struct SwsContext *swsContext);

示例

int FFDecoder::videoConvert(AVFrame* pFrame, uint8_t* out){
    if(pFrame == NULL || out == NULL) 
        return 0;
    
    //AVPicture pict;
    //av_image_alloc(pict.data, pict.linesize, pVCodecCxt->width,pVCodecCxt->height, AV_PIX_FMT_RGBA, 16);
    uint8_t* dst_data[4] = {0};
    char *swsData = new char[720*576*4];
    dst_data[0] =(uint8_t *)swsData;
    
    int dst_linesize[4];
    dst_linesize[0] =  720*4;
    swsContext = sws_getCachedContext(swsContext,pFrame->width, pFrame->height,(AVPixelFormat)pFrame->format,
                                      720, 576, AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR, 0, 0, 0);
    if(swsContext){
        sws_scale(swsContext, (const uint8_t**)pFrame->data, pFrame->linesize, 0, pFrame->height, (uint8_t* const*)dst_data, dst_linesize);
        size_t size = dst_linesize[0] * 576;
        memcpy(out,dst_data[0],size);
        //av_free(&pict.data[0]);
        return size;
    }
    
    //av_free(&pict.data[0]);
    return 0;
}

2、使用SurfaceView显示

转换后的视频数据使用SurfaceView显示,可以在JAVA层处理,也可以在C++层,我们使用的是C++层,实际上使用的是Android的ANativeWindow来进行处理。
所以我们需要在JAVA层创建SurfaceView,获取surface后传入到JNI层,在JNI层通过surface获取ANativeWindow来显示。

JAVA层创建SurfaceView,并传入surface:
为确保surface已经创建,在surfaceChanged或者surfaceCreated的回调函数中,才将surface设置下去。
SurfaceView的创建不用多说,在布局文件添加相关控件即可。
如下:

    private void initView() {
      mSurfaceView = (SurfaceView)findViewById(R.id.surfaceView); 
      mSurfaceHolder = mSurfaceView.getHolder();
      mSurfaceHolder.addCallback(new Callback() {
      
		@Override
		public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
			if(mFFdec != null){
				mFFdec.initSurface(mSurfaceHolder.getSurface());
			}
		}
		@Override
		public void surfaceCreated(SurfaceHolder arg0) {}
		@Override
		public void surfaceDestroyed(SurfaceHolder arg0) {}
		});
    }
    

mFFdec.initSurface(mSurfaceHolder.getSurface());是native接口,将surface设置到JNI层,具体实现如下:

public  native void initSurface(Surface surface);

......仅列出关键代码
static ANativeWindow* g_nwin = NULL;
static void jni_initSurface(JNIEnv* env, jobject obj, jobject surface){
	if(surface){
		g_nwin = ANativeWindow_fromSurface(env, surface);
		if(g_nwin){
			ANativeWindow_setBuffersGeometry(g_nwin, 720, 576, WINDOW_FORMAT_RGBA_8888);
			LOGE("jni_initSurface g_nwin[%p],g_nwin->perform[%p]\n",g_nwin,g_nwin->perform);
		}
	}
    return;
}

static JNINativeMethod gMethods[] = {
    {"decodeInit", "()V", (void*)jni_decodeInit},
    {"decodeDeInit", "()V", (void*)jni_decodeDeInit},
    {"decodeFrame", "([B)I", (void*)jni_decodeFrame},
    {"openInput", "(Ljava/lang/String;)I", (void*)jni_openInput},
    {"getMediaSampleRate", "()I", (void*)jni_getMediaSampleRate},
    {"getMediaType", "()I", (void*)jni_getMediaType},
    {"initSurface", "(Landroid/view/Surface;)V", (void*)jni_initSurface},
};
...省略

ANativeWindow使用流程

1、初始化:
调用ANativeWindow_fromSurface从JAVA层传入的surface获取ANativeWindow;
调用ANativeWindow_setBuffersGeometry设置ANativeWindow相关参数,包括宽/高/像素格式(注意要和我们传入的数据匹配)。
2、锁定:
ANativeWindow是双缓冲机制,先调用ANativeWindow_lock锁定后台的缓冲部分,并获取surface缓冲区的地址。
3、绘制:
往缓冲区填充数据后,调用ANativeWindow_unlockAndPost,即会解锁缓冲区并绘制。

示例

static int renderVframe(uint8_t* rgb,int size){
    if(g_nwin == NULL)
        return -1;

    LOGE("renderVframe g_nwin[%p],g_nwin->perform[%p]\n",g_nwin,g_nwin->perform);
    ANativeWindow_Buffer outBuffer;

    ANativeWindow_lock(g_nwin, &outBuffer,0);//获取surface缓冲区的地址
    uint8_t* dst = (uint8_t*)outBuffer.bits;
    memcpy(dst,rgb,size);//往surface缓冲区填充显示的RGB内容
    ANativeWindow_unlockAndPost(g_nwin);

    return 0;
}

猜你喜欢

转载自blog.csdn.net/myvest/article/details/90717333