视频编解码学习一 yuv格式

  最近研究直播,一路下来遇到了很多问题,但最终经过半年的学习,在音视频这块已经有了进一步的理解,感谢我的朋友们给予的帮助,打算写下这半年来总结出来的经验,让更多的人能够接触到音视频这块。

  今天说下视频解码基础知识
  众所周知,我们日常看到的视频都有各种各样的视频格式,mp4,flv,rmvb,mkv,wmv。。。这些视频是根据什么区分的,为什么会有这么多视频格式。
  首先说下视频压缩,视频都是一帧一帧的图片组成,他们都是通过容器封装成各种各样的格式。人眼的视觉残留特性导致人眼的分辨率不能超过30帧每秒,那么我们也就没有必要按照视频最初采集的大小进行存储,否则存储消耗实在是太大,比如1080p电影15分钟经过YUV4:2:0格式下的数据量为1920 x 1028 x 12 x 25 x 15 x 60/8/1024/1024/1024=62.03GB。这对于拍摄一部90分钟的电影来说简直是噩梦。

  在摄像头之类编程经常是会碰到YUV格式,而非大家比较熟悉的RGB格式. 我们可以把YUV看成是一个RGB的变种来理解。YUV的原理是把亮度与色度分离,研究证明,人眼对亮度的敏感超过色度。利用这个原理,可以把色度信息减少一点,人眼也无法查觉这一点。
  YUV三个字母中,其中"Y"表示明亮度(Lumina nce或Luma),也就是灰阶值;而"U"和"V"表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。用这个三个字母好象就是通道命令。

  那么我们如果将本地视频解码并保存为yuv格式呢?在这里FFmpeg官方给出的视频解码流程

FFmpeg解码

从上面流程图我们可以看出视频解码流程
第一步:注册所有组件
第二步:打开视频输入文件
第三步:查找视频文件信息
第四步:查找解码器
第五步:打开解码器
第六步:循环读取视频帧,进行循环解码
第七步:关闭解码组件

下面我们就来实现使用FFmpeg将视频解码为yuv格式

源码

#include <jni.h>
#include <string>
//导入android-log日志
#include <android/log.h>

//当前C++兼容C语言
extern "C"{
//avcodec:编解码(最重要的库)
#include "libavcodec/avcodec.h"
//avformat:封装格式处理
#include "libavformat/avformat.h"
//avutil:工具库(大部分库都需要这个库的支持)
#include "libavutil/imgutils.h"
//swscale:视频像素数据格式转换
#include "libswscale/swscale.h"
//导入音频采样数据格式转换库
#include "libswresample/swresample.h"

JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegTest
        (JNIEnv *, jobject);
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegDecoder
        (JNIEnv *env, jobject jobj,jstring jInFilePath,jstring jOutFilePath);
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegDecoderAudio
        (JNIEnv *env, jobject jobj,jstring jInFilePath,jstring jOutFilePath);
}

//1、NDK音视频编解码:FFmpeg-测试配置
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegTest(
        JNIEnv *env, jobject jobj) {
    //(char *)表示C语言字符串
    const char *configuration = avcodec_configuration();
    __android_log_print(ANDROID_LOG_INFO,"main","%s",configuration);
}


//2.NDK音视频编解码:FFmpeg-视频解码-视频像素数据(YUV420P)
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegDecoder(
        JNIEnv *env, jobject jobj, jstring jinputFilePath, jstring joutputFilePath) {

    //将java->string类型->C字符串->char*
    const char* cinputFilePath = env->GetStringUTFChars(jinputFilePath,NULL);
    const char* coutputFilePath = env->GetStringUTFChars(joutputFilePath,NULL);

    //第一步:注册所有组件
    av_register_all();

    //第二步:打开视频输入文件
    //参数一:封装格式上下文->AVFormatContext->包含了视频信息(视频格式、大小等等...)
    AVFormatContext* avformat_context = avformat_alloc_context();
    //参数二:打开文件(入口文件)->url
    int avformat_open_result = avformat_open_input(&avformat_context,cinputFilePath,NULL,NULL);
    if (avformat_open_result != 0){
        //获取异常信息
        char* error_info;
        av_strerror(avformat_open_result, error_info, 1024);
        __android_log_print(ANDROID_LOG_INFO,"main","异常信息1:%s",error_info);
        return;
    }


    //第三步:查找视频文件信息
    //参数一:封装格式上下文->AVFormatContext
    //参数二:配置
    //返回值:0>=返回OK,否则失败
    int avformat_find_stream_info_result = avformat_find_stream_info(avformat_context, NULL);
    if (avformat_find_stream_info_result < 0){
        //获取失败
        char* error_info;
        av_strerror(avformat_find_stream_info_result, error_info, 1024);
        __android_log_print(ANDROID_LOG_INFO,"main","异常信息:%s",error_info);
        return;
    }


    //第四步:查找解码器
    //第一点:获取当前解码器是属于什么类型解码器->找到了视频流
    //音频解码器、视频解码器、字幕解码器等等...
    //获取视频解码器流引用->指针
    int av_stream_index = -1;
    for (int i = 0; i < avformat_context->nb_streams; ++i) {
        //循环遍历每一流
        //视频流、音频流、字幕流等等...
        if (avformat_context->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){
            //找到了
            av_stream_index = i;
            break;
        }
    }
    if (av_stream_index == -1){
        __android_log_print(ANDROID_LOG_INFO,"main","%s","没有找到视频流");
        return;
    }

    //第二点:根据视频流->查找到视频解码器上下文->视频压缩数据
    AVCodecContext* avcodec_context = avformat_context->streams[av_stream_index]->codec;

    //第三点:根据解码器上下文->获取解码器ID
    AVCodec* avcodec = avcodec_find_decoder(avcodec_context->codec_id);
    if (avcodec == NULL){
        __android_log_print(ANDROID_LOG_INFO,"main","%s","没有找到视频解码器");
        return;
    }

    //第五步:打开解码器
    int avcodec_open2_result = avcodec_open2(avcodec_context,avcodec,NULL);
    if (avcodec_open2_result != 0){
        char* error_info;
        av_strerror(avcodec_open2_result, error_info, 1024);
        __android_log_print(ANDROID_LOG_INFO,"main","异常信息:%s",error_info);
        return;
    }

    //输出视频信息
    //输出:文件格式
    __android_log_print(ANDROID_LOG_INFO,"main","文件格式:%s",avformat_context->iformat->name);
    //输出:解码器名称
    __android_log_print(ANDROID_LOG_INFO,"main","解码器名称:%s",avcodec->name);


    //第六步:循环读取视频帧,进行循环解码->输出YUV420P视频->格式:yuv格式

    //读取帧数据换成到哪里->缓存到packet里面
    AVPacket* av_packet = (AVPacket*)av_malloc(sizeof(AVPacket));

    //输入->环境一帧数据->缓冲区->类似于一张图
    AVFrame* av_frame_in = av_frame_alloc();
    //输出->帧数据->视频像素数据格式->yuv420p
    AVFrame* av_frame_out_yuv420p = av_frame_alloc();

    //解码的状态类型(0:表示解码完毕,非0:表示正在解码)
    int got_picture_ptr, av_decode_result, y_size, u_size, v_size, current_frame_index = 0;

    //准备一个视频像素数据格式上下文
    //参数一:输入帧数据宽
    //参数二:输入帧数据高
    //参数三:输入帧数据格式
    //参数四:输出帧数据宽
    //参数五:输出帧数据高
    //参数六:输出帧数据格式->AV_PIX_FMT_YUV420P
    //参数七:视频像素数据格式转换算法类型
    //参数八:字节对齐类型(C/C++里面)->提高读取效率
    SwsContext* sws_context = sws_getContext(avcodec_context->width,
                                             avcodec_context->height,
                                             avcodec_context->pix_fmt,
                                             avcodec_context->width,
                                             avcodec_context->height,
                                             AV_PIX_FMT_YUV420P,
                                             SWS_BICUBIC,NULL,NULL,NULL);


    //打开文件
    FILE* out_file_yuv = fopen(coutputFilePath,"wb+");
    if (out_file_yuv == NULL){
        __android_log_print(ANDROID_LOG_INFO,"main","文件不存在");
        return;
    }


    //>=0:说明有数据,继续读取
    //<0:说明读取完毕,结束
    while (av_read_frame(avformat_context,av_packet) >= 0){
        //解码什么类型流(视频流、音频流、字幕流等等...)
        if (av_packet->stream_index == av_stream_index){

            //扩展知识面(有更新)
            //解码一帧视频流数据
            //分析:avcodec_decode_video2函数
            //参数一:解码器上下文
            //参数二:一帧数据
            //参数三:got_picture_ptr->是否正在解码(0:表示解码完毕,非0:表示正在解码)
            //参数四:一帧压缩数据(对压缩数据进行解码操作)
            //返回值:av_decode_result == 0表示解码一帧数据成功,否则失败
            //av_decode_result = avcodec_decode_video2(avcodec_context,av_frame_in,&got_picture_ptr,av_packet);

            //新的API操作
            //发送一帧数据->接收一帧数据

            //发送一帧数据
            avcodec_send_packet(avcodec_context, av_packet);

            //接收一帧数据->解码一帧
            av_decode_result = avcodec_receive_frame(avcodec_context, av_frame_in);

            //解码出来的每一帧数据成功之后,将每一帧数据保存为YUV420格式文件类型(.yuv文件格式)
            if ( av_decode_result == 0 ){
                //sws_scale:作用将视频像素数据格式->yuv420p格式
                //输出.yuv文件->视频像素数据格式文件->输出到文件API
                //参数一:视频像素数据格式->上下文
                //参数二:输入数据
                //参数三:输入画面每一行的大小
                //参数四:输入画面每一行的要转码的开始位置
                //参数五:每一帧数据高
                //参数六:输出画面数据
                //参数七:输出画面每一行的大小
                sws_scale(sws_context,
                          (const uint8_t *const*)av_frame_in->data,
                          av_frame_in->linesize,
                          0,
                          avcodec_context->height,
                          av_frame_out_yuv420p->data,
                          av_frame_out_yuv420p->linesize);


                //一帧一帧写入文件->yuv420p->视频像素数据格式
                //第一点:分析yuv420p格式原理
                //写入文件:按照像素点位置来进行写入->将av_frame_out_yuv420p一帧数据一个个字节读取
                //普及一下YUV420格式(人对眼睛亮度敏感,对色度不敏感)
                //Y代表:亮度
                //UV代表:色度
                //第二点:分析yuv420规则->计算机图像原理(听老师讲解于原理->扩展知识面)->直播技术
                //yuv420规则一:Y结构表示一个像素点
                //yuv420规则二:四个Y对应一个U和一个V(也就是四个像素点,对应一个U和一个V)
                //第三点:分析Y和U、V大小计算原理
                // y = 宽 * 高
                // u = y / 4
                // v = y / 4
                y_size = avcodec_context->width * avcodec_context->height;
                u_size = y_size / 4;
                v_size = y_size / 4;


                //第四点:写入文件
                //写入->Y
                //av_frame_in->data[0]:表示Y
                fwrite(av_frame_in->data[0], 1, y_size, out_file_yuv);
                //写入->U
                //av_frame_in->data[1]:表示U
                fwrite(av_frame_in->data[1], 1, u_size, out_file_yuv);
                //写入->V
                //av_frame_in->data[2]:表示V
                fwrite(av_frame_in->data[2], 1, v_size, out_file_yuv);

                current_frame_index++;

                __android_log_print(ANDROID_LOG_INFO,"main","当前遍历第%d帧",current_frame_index);

            }

        }
    }


    //第七步:关闭解码组件->释放内存
    av_packet_free(&av_packet);
    //关闭流
    fclose(out_file_yuv);
    av_frame_free(&av_frame_in);
    av_frame_free(&av_frame_out_yuv420p);
    avcodec_close(avcodec_context);
    avformat_free_context(avformat_context);

}

上述代码实现完成之后可以在pc端下载yuv player来直接播放,还有一点注意就是设置屏幕大小必须设置对,否则会出现花屏。关于如何在移动端实现播放本地yuv视频,可以参考视频编解码学习三 播放yuv格式视频

下载

https://github.com/samychen/SDL_FFmpeg_Tutorial

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

推荐阅读更多精彩内容

  • 前言 说到视频,大家自己脑子里基本都会想起电影、电视剧、在线视频等等,也会想起一些视频格式 AVI、MP4、RMV...
    ForestSen阅读 22,750评论 10 203
  • [TOC] 音视频&流媒体 是什么促使我要写这一篇音视频入门文章?那是因为和一妹子打赌码率的概念,结果输了;对一个...
    AllenWu阅读 4,743评论 1 25
  • 教程一:视频截图(Tutorial 01: Making Screencaps) 首先我们需要了解视频文件的一些基...
    90后的思维阅读 4,628评论 0 3
  • 第1章介绍 1. 为什么要进行视频压缩? 未经压缩的数字视频的数据量巨大 存储困难 一张DVD只能存储几秒钟的未压...
    ISunshine阅读 721评论 0 11
  • UI卡顿原理 60 fps -->16ms卡顿,主要是渲染。一秒60帧, 在16ms内完成计算渲染,操作。在绘制的...
    刘尔泽阅读 237评论 0 0