FFmpeg视频播放--YUV输出

之前用的Android SurfaceView播放视频是采用的把surface丢到JNI层,在里面更新视图,这种方式只能渲染 AV_PIX_FMT_RGBA 的格式。但是,由于FFmpeg解码出来的格式默认是YUV的数据,所以解码出来之后我们要转换成为RGBA的,这个转换的操作是很耗时和耗性能的,所以就需要直接使用YUV数据。

1、了解YUV数据来源

首先我们要知道,不管是YUV 还是RGBA或者其他的格式,每一帧数据都是存储在AVFrame里面的,那么我们就要先了解一下AVFrame。
关于AVFrame,网上有很多的介绍,我这里也不多说,这里给出雷神关于AVFrame的讲解:FFMPEG结构体分析:AVFrame

了解过AVFrame之后,我们知道有两个很重要的数组:

/**
 * pointer to the picture/channel planes.
 * 图像数据
 * This might be different from the first allocated byte
 */
uint8_t *data[AV_NUM_DATA_POINTERS];
/**
 * For video, size in bytes of each picture line.
 * 对于视频,每一帧图象一行的字节大小。
 * For audio, size in bytes of each plane.
 */

int linesize[AV_NUM_DATA_POINTERS];

雷神说:

uint8_t *data[AV_NUM_DATA_POINTERS]:解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
int linesize[AV_NUM_DATA_POINTERS]:data中“一行”数据的大小。
注意:未必等于图像的宽,一般大于图像的宽

所以很清楚,我们的数据就是从这两个数据里面来获取YUV数据。
如何在C/C++层获取YUV数据,参考雷神的另外一篇文章:FFMPEG 实现 YUV,RGB各种图像原始数据之间的转换(swscale)

我这里只贴出关键代码,具体的去看雷神的帖子

//YUV420P 
fwrite(pFrameYUV->data[0],(pCodecCtx->width)*(pCodecCtx->height),1,output); 
fwrite(pFrameYUV->data[1],(pCodecCtx->width)*(pCodecCtx->height)/4,1,output); 
fwrite(pFrameYUV->data[2],(pCodecCtx->width)*(pCodecCtx->height)/4,1,output); 

根据上面的代码我们知道,
Y的数据的长度=视频的原始宽(pCodecCtx->width) × 视频的原始高度(pCodecCtx->height)
u的数据的长度 = v = y/4

2、编写代码

知道这些之后,开始写我们今天的代码

2.1、定义方法:
/**
 * JNI 回调视频的宽度和高度
 */  
private void setMediaSize(int width, int height) { }
/**
 * JNI 回调每一帧的YUV数据
 */
private void onDecoder(byte[] yData, byte[] uData, byte[] vData) {}

注:这里主要讲YUV的数据的回调,所以以下代码无关乎 setMediaSize(int width, int height)。

2.2、找到Java类里面的方法:
//定义Java类的包名和类名
const char *J_CLASS_NAME = "com/eson/player/MyFPlayerCore";
jclass playerCore; 
//java方法
jmethodID onDecoder;

playerCore =jniEnv->FindClass(J_CLASS_NAME);
onDecoder = jniEnv->GetMethodID(playerCore, "onDecoder", "([B[B[B)V");
2.3、获取YUV数据

我们还是用之前写的onDecoder(AVFrame *avFrame)这个方法,只需要修改一下就行。

JNIEnv *jniEnv;
jbyteArray yArray;
jbyteArray uArray;
jbyteArray vArray;
int length = 0;
unsigned char *ydata;
unsigned char *udata;
unsigned char *vdata;

void VideoCallBack::onDecoder(AVFrame *avFrame) {
//    LOGE("onDecoder (AVFrame)");
    if (w_width == 0 || w_height == 0) {
        return;
    }
    if (!avFrame) {
        return;
    }
    //这里只是获取到数据的指针
    ydata = avFrame->data[0];
    udata = avFrame->data[1];
    vdata = avFrame->data[2];

  //刚开始读数据前几帧数据有空数据,不知道为什么
    if (ydata == NULL || udata == NULL || vdata == NULL) {
        return;
    }

  //数据的长度,即Java byte[] 的长度
    if (length == 0) {
        length = w_width * w_height;
    }
  
    if (jniEnv == NULL) {
        jniEnv = callJavaUtil->getCurrentJNIEnv();
        LOGE(" got new jnienv");
    }
    //只初始化一次长度
    if (yArray == NULL) {
        yArray = jniEnv->NewByteArray(length);
        LOGE(" got new yArray");
    }
  
    jniEnv->SetByteArrayRegion(yArray, 0, length, (jbyte *) ydata);
    if (uArray == NULL) {
        uArray = jniEnv->NewByteArray(length / 4);
        LOGE(" got new uArray");
    }
   
    jniEnv->SetByteArrayRegion(uArray, 0, length / 4, (jbyte *) udata);
    if (vArray == NULL) {
        vArray = jniEnv->NewByteArray(length / 4);
        LOGE(" got new vArray");
    }
 
    jniEnv->SetByteArrayRegion(vArray, 0, length / 4, (jbyte *) vdata);
    //回调
    callJavaUtil->callOnDecoder(jniEnv, yArray, uArray, vArray);
}

我们再打印一下linesize的长度,看一下与视频宽度(w_height)的关系

LOGE("avFrame->linesize[0] ----->>>%d",avFrame->linesize[0]);
LOGE("avFrame->linesize[1] ----->>>%d",avFrame->linesize[1]);
LOGE("avFrame->linesize[2] ----->>>%d",avFrame->linesize[2]);

经过几个视频的测试会发现,avFrame->linesize[0] 始终是avFrame->linesize[1]和avFrame->linesize[2]的2倍,而avFrame->linesize[0] 和w_height是相等的。可雷神说是不总是相等的,那怎么办?

2.4、完善

后来经过查资料,在这里找到了解决方法:ffmpeg从AVFrame取出yuv数据到保存到char*中

参照他的方法我对代码进行了修改:

JNIEnv *jniEnv;
jbyteArray yArray;
jbyteArray uArray;
jbyteArray vArray;
int length = 0;
unsigned char *ydata;
unsigned char *udata;
unsigned char *vdata;

//新的yuv数据
uint8_t *newY = NULL;
uint8_t *newU = NULL;
uint8_t *newV = NULL;

void VideoCallBack::onDecoder(AVFrame *avFrame) {
//    LOGE("onDecoder (AVFrame)");
    if (w_width == 0 || w_height == 0) {
        return;
    }
    if (!avFrame) {
        return;
    }
    ydata = avFrame->data[0];
    udata = avFrame->data[1];
    vdata = avFrame->data[2];
    if (ydata == NULL || udata == NULL || vdata == NULL) {
        return;
    }

    //长度不变,不改变原始图像的宽高
    if (length == 0) {
        length = w_width * w_height;
    }
    //重新申请一个与所需相同的内存
    if (newY == NULL) {
        newY = (uint8_t *) av_malloc(length * sizeof(uint8_t));
    }
    if (newU == NULL) {
        newU = (uint8_t *) av_malloc(length / 4 * sizeof(uint8_t));
    }
    if (newV == NULL) {
        newV = (uint8_t *) av_malloc(length / 4 * sizeof(uint8_t));
    }

    //把原始数据复制到申请的内存里面
    for (int i = 0; i < w_height; i++) {
        memcpy(newY + w_width * i,
               ydata + avFrame->linesize[0] * i,
               w_width);
    }
    for (int j = 0; j < w_height / 2; j++) {
        memcpy(newU + w_width / 2 * j,
               udata + avFrame->linesize[1] * j,
               w_width / 2);
    }
    for (int k = 0; k < w_height / 2; k++) {
        memcpy(newV + w_width / 2 * k,
               vdata + avFrame->linesize[2] * k,
               w_width / 2);
    }
    if (jniEnv == NULL) {
        jniEnv = callJavaUtil->getCurrentJNIEnv();
    }
    //只初始化一次长度
    if (yArray == NULL) {
        yArray = jniEnv->NewByteArray(length);
    }
   
    //把新的数据放到byte[]里面
    jniEnv->SetByteArrayRegion(yArray, 0, length, (jbyte *) newY);
    if (uArray == NULL) {
        uArray = jniEnv->NewByteArray(length / 4);
    }
    if (uArray == NULL){
        return;
    }
    jniEnv->SetByteArrayRegion(uArray, 0, length / 4, (jbyte *) newU);
    if (vArray == NULL) {
        vArray = jniEnv->NewByteArray(length / 4);
    }
    if (vArray == NULL){
        return;
    }
    jniEnv->SetByteArrayRegion(vArray, 0, length / 4, (jbyte *) newV);
    //回调
    callJavaUtil->callOnDecoder(jniEnv, yArray, uArray, vArray);
}

到这里,AVFrame里面的YUV数据就以byte[]的方式传递到了Java层了。

最后说一下,由于之前公司离职,所以关于视频解码这一块的文章估计会停更,也或许不会,看情况吧。

你可能感兴趣的:(FFmpeg视频播放--YUV输出)