美文网首页流媒体ffmpeg
视频编解码学习一 yuv格式

视频编解码学习一 yuv格式

作者: samychen | 来源:发表于2017-11-25 22:53 被阅读0次

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

      今天说下视频解码基础知识
      众所周知,我们日常看到的视频都有各种各样的视频格式,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

    相关文章

      网友评论

        本文标题:视频编解码学习一 yuv格式

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