视频编解码学习一 yuv格式

  • Post author:
  • Post category:其他


今天说下视频解码基础知识

众所周知,我们日常看到的视频都有各种各样的视频格式,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