FFmpeg视频播放--YUV输出

作者: esonyf | 来源:发表于2016-10-02 13:18 被阅读2343次

    之前用的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层了。

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

    相关文章

      网友评论

      • 过期的薯条:大佬。我有一个yuv文件。通过文件流的读取 获得其byte[]。根据网上的rawByteArray2RGBABitmap2 将其转化成bitmap。但是显示的图片 明显是没有处理好。请问怎么回事,能否提供一段代码看下
      • 79354670891a:解码和回调 之间的线程如何处理呢?
        79354670891a:@esonyf 请问下,实时视频有延迟,是不是因为有缓存?可以通过ffmpeg中哪些参数设置呢
        79354670891a:@esonyf 好的,已经解决了。谢谢
        esonyf:@O_o__Cm 你是想问解码线程如何回调出去的吗,还是什么?你可以参考我的上一遍文章:http://www.jianshu.com/p/4b60cea7fa85,希望对你有帮助

      本文标题:FFmpeg视频播放--YUV输出

      本文链接:https://www.haomeiwen.com/subject/cnmhyttx.html