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层了。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,601评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,367评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,249评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,539评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,967评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,929评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,098评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,825评论 0 207
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,609评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,796评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,282评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,603评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,277评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,159评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,959评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,079评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,874评论 2 277

推荐阅读更多精彩内容

  • 教程一:视频截图(Tutorial 01: Making Screencaps) 首先我们需要了解视频文件的一些基...
    90后的思维阅读 4,519评论 0 3
  • 前言 代码github地址https://github.com/ccj659/NDK-FFmpeg-master ...
    Chauncey_Chen阅读 3,785评论 3 31
  • 本篇博客在雷神的结构体介绍基础上按自己的喜好整理的 后面根据自己工作中所需有所增改 AVStream 存储每一个视...
    石丘阅读 2,416评论 1 10
  • 就地取材广廈闲, 清风满院彩云翩。 采得野味留远客, 共语蓝天乐忘还。
    秋雨敲窗阅读 117评论 0 0
  • 烦躁的时候总是会控制不住自己,心塞
    童Toni阅读 136评论 0 0