天天看点

FFmpeg视频解码为YUV像素数据文件

视频解码API调用流程图

FFmpeg视频解码为YUV像素数据文件
**FFmpeg解码函数**
av_register_all():注册所有组件
avformat_open_input():打开输入视频文件
avformat_find_stream_info():获取视频文件信息
avcodec_find_decoder():查找解码器
avcodec_open2():打开解码器
avcodec_alloc_context3():获取解码器上下文(FFmpeg 以后的API)
avcodec_parameters_to_context():为解码器上下文获取视频信息FFmpeg 以后的API)
av_read_frame():从输入文件读取一帧压缩数据
avcodec_decode_video2():解码一帧压缩数据
avcodec_close():关闭解码器
avformat_close_input():关闭输入视频文件
           

FFmpeg解码的数据结构如下图所示

FFmpeg视频解码为YUV像素数据文件

FFmpeg数据结构简介

AVFormatContext

封装格式上下文结构体,也是统称全局的结构体,保存了视频文件封装格式相关信息

AVInputFormat

每种封装格式(例如:FLV、MKV、MP4、AVI等)对应一个该结构体

AVStream

视频文件中每个视频(音频)流对应一个该结构体

AVCodecContext

编码器上下文结构体,保存了视频(音频)编解码相关的信息

AVCodec

每种视频(音频)编解码器(例如:H.264解码器)对应一个该结构体。

AVPacket

存储一帧压缩编码数据

AVFrame

存储一帧解码后像素(采样)数据。

FFmpeg数据结构分析

AVFormatContext

iformat:输入视频的AVInputFormat

nb_streams:输入视频的AVStream个数

streams:输入视频的AVStream[]数组

druation:输入视频的时长(以微秒为单位)

bit_rate:输入视频码率

AVInputFormat

name:封装格式名称

long_name:封装格式的长名称

extensions:封装格式的扩展名

id:封装格式的ID

一些封装格式处理的接口函数

AVStream

id:序号

codec:该流对应的AVCodecContext

time_base:该流的时基

r_frame_rate:该流的帧率

AVCodecContext

codec:编解码器的AVCodec

width,height:图像的宽高(只针对视频)

pix_fmt:像素格式(只针对视频)

sample_rae:采样率(只针对音频)

channels:声道数(只针对音频)

sample_fmt:采样格式(只针对音频)

AVCodec

name:编解码器名称

long_name:编解码器的长名称

type:编解码器类型

id:编解码器ID

一些编解码的接口函数

AVPacket

pts:显示时间戳

dts:解码时间戳

data:压缩编码数据

size:压缩编码数据大小

stream_index:所属的AVStream

AVFrame

data:解码后的图像像素数据(音频采样数据)

linesize:对视频来说是图像中一行像素的大小;对应音频来说是整个音频帧的大小

width,height:图像的宽高(只针对视频)

key_frame:是否为关键帧(只针对视频)

pict_type:帧类型(值针对视频)。例如:I,P,B

补充知识

解码后的数据为什么要经过sws_scale()函数处理?

解码后YUV格式的视频像素数据保存在AVFrame的data[0]、data[1]、data[2]中。但是这些像素值并不是连续存储的,每行有效像素之后存储了一些无效像素。以亮度Y数据为例,data[0]中一共包含了linesize[0]*height个数据。但是出于优化等方面的考虑,linesize[0]实际上并不等于宽度width,而是一个比宽度大一些的值。因此需要使用sws_scale()进行转换。转换后去除了无效数据,width和linesize[0]取值相等。

FFmpeg视频解码为YUV像素数据文件
#include "com_xy_ndk_ffmpeg_NDKFFmpeg.h"
#include <android/log.h>

#define LOG_I_ARGS(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"FFmpeg_cpp",FORMAT,__VA_ARGS__);
#define LOG_I(FORMAT) LOG_I_ARGS(FORMAT,0);

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

JNIEXPORT void JNICALL Java_com_xy_ndk_ffmpeg_NDKFFmpeg_callFFmpegNewDecode
        (JNIEnv *env, jobject, jstring jInputFilePath, jstring jOutputFilePath) {
//将jstring转成C的字符串
    //视频输入地址
    const char* cInFilePath = env->GetStringUTFChars(jInputFilePath,NULL);
    //解码之后的输出文件地址
    const char* cOutFilePath = env->GetStringUTFChars(jOutputFilePath,NULL);

    //接下来就是读取视频信息
    //分析音视频解码流程
    //第一步:注册组件
    av_register_all();

    //第二步:打开输入视频文件
    //初始化封装格式上下文
    AVFormatContext* avFmtCtx = avformat_alloc_context();
    int fmt_open_result = avformat_open_input(&avFmtCtx,cInFilePath,NULL,NULL);
    if(fmt_open_result != ){
        LOG_I("打开视频文件失败");
        return;
    }

    //第三步:获取视频文件信息(文件流)
    //很多流(例如:视频流、音频流、字幕流等等......)
    //然后我的目的:我只需要视频流信息
    int fmt_fd_info = avformat_find_stream_info(avFmtCtx,NULL);
    if(fmt_fd_info < ){
        LOG_I("获取视频文件信息失败");
        //打印错误码
        //错误信息
        char* error_info;
        //根据错误码找到对应的错误信息s
        av_strerror(fmt_fd_info,error_info,);
        LOG_I_ARGS("错误信息:%s",error_info);
        return;
    }

    //第四步:查找解码器
    //1.获取视频流的索引位置
    //遍历所有的流,找到视频流
    int av_stream_index = -;
    //avFmtCtx->nb_streams:返回流的大小
    for (int i = ; i < avFmtCtx->nb_streams; ++i) {
        //判断流的类型(老的API实现)
        //是否是视频流
        if(avFmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
            av_stream_index = i;
            break;
        }
    }
    if(av_stream_index == -){
        LOG_I("不能存在视频流......");
        return;
    }

    //2.根据视频流的索引位置,查找视频流的解码器
    //根据视频流的索引位置,获取到指定的参数上下文
    //编码方式(编码上下文)
//
 AVCodecParameters *avParams= avFmtCtx->streams[av_stream_index]->codecpar;
    AVCodec *avCd = avcodec_find_decoder(avParams->codec_id);
    if(avCd == NULL){
        LOG_I("没有找到这个解码器");
        return;
    }
    AVCodecContext *avCdCtx = avcodec_alloc_context3(avCd);
    //根据所提供的编解码器的值填充编译码上下文
    int avcodec_to_context = avcodec_parameters_to_context(avCdCtx,avParams);
    if(avcodec_to_context < ){
        return;
    }
    //第五步:打开解码器
    int av_cd_open_result = avcodec_open2(avCdCtx,avCd,NULL);
    if(av_cd_open_result != ){
        LOG_I("解码器打开失败......");
        return;
    }

    //获取配置视频信息
    //文件格式、文件的宽高、解码器的名称等等......
    LOG_I_ARGS("视频文件的格式:%s",avFmtCtx->iformat->name);
    //返回的单位是:微秒(avFmtCtx->duration)
    LOG_I_ARGS("视频的时长:%lld秒",(avFmtCtx->duration)/);
    //获取宽高
    LOG_I_ARGS("视频的宽高:%d x %d = ",avCdCtx->width,avCdCtx->height);
    //解码器的名称
    LOG_I_ARGS("解码器的名称:%s",avCd->name);


    //第六步:从输入文件读取一帧压缩数据(解压缩:一帧一帧读取解压缩)
    //循环读取每一帧数据
    //读取的帧数据缓存到那里(开辟一块内存空间用于保存)
    AVPacket* packet = (AVPacket*)av_malloc(sizeof(AVPacket));
    //缓存一帧数据(就是一张图片)
    AVFrame* in_frame_picture = av_frame_alloc();

    //定义输出一帧数据(缓冲区:YUV420p类型)
    AVFrame* out_frame_picture_YUV42P = av_frame_alloc();
    //指定缓冲区的类型(像素格式:YUV420P)
    //老API
    //开启空间的大小是:YUV420P格式数据大小
//    uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P,avCdCtx->width,avCdCtx->height));
    //指定填充数据(YUV420P数据)
//    avpicture_fill((AVPicture *)out_frame_picture_YUV42P,out_buffer,AV_PIX_FMT_YUV420P,avCdCtx->width,avCdCtx->height);

    //新API
    //av_image_get_buffer_size: 计算缓冲区的大小
    //参数一:缓冲区格式(需要:AV_PIX_FMT_YUV420P)
    //参数二:缓冲区宽度
    //参数三:缓冲区高度
    //参数四:字节对齐的方式(通用设置:1)
    //回忆:以前学习结构体的大小的时候,讲解过字节对齐(字节对齐目的:为了提高读取的效率或者说性能)
    int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P,avCdCtx->width,avCdCtx->height,);
    //开辟缓存空间
    uint8_t *out_buffer = (uint8_t *)av_malloc(buffer_size);
    //av_image_fill_arrays: 指定缓冲区填充的像素数据类型(要求:YUV420P视频像素数据格式)
    //参数分析
    //参数一:填充的数据
    //参数二:每一行大小
    //参数三:缓冲区
    //参数四:填充的像素数据格式(在这里我们要求:YUV420P)
    //参数五:每一帧宽度(视频宽)
    //参数六:每一帧高度(视频高)
    //参数七:字节对齐的方式(通用设置:1)
    av_image_fill_arrays(out_frame_picture_YUV42P->data,out_frame_picture_YUV42P->linesize,out_buffer
            ,AV_PIX_FMT_YUV420P,avCdCtx->width,avCdCtx->height,);

    int ret,y_size = ,u_size = ,v_size = ,frame_index = ;

    //打开文件
    FILE* out_file_yuv = fopen(cOutFilePath,"wb");
    if(out_file_yuv == NULL){
        LOG_I("文件不存在!");
        return;
    }
    //视频像素数据格式转换上下文
    //参数一:输入的宽度
    //参数二:输入的高度
    //参数三:输入的数据
    //参数四:输出的宽度
    //参数五:输出的高度
    //参数六:输出的数据
    //参数七:视频像素数据格式转换算法类型(使用什么算法)
    //参数八:字节对齐类型,一般都是默认1(字节对齐类型:提高读取效率)
    SwsContext *sws_ctx =
            sws_getContext(avCdCtx->width,avCdCtx->height, avCdCtx->pix_fmt,
                           avCdCtx->width,avCdCtx->height,AV_PIX_FMT_YUV420P,
                           SWS_BICUBIC,NULL,NULL,NULL);

    //读取返回值
    //>=0:正在读取
    //<0:读取失败或者说读取完毕
    while (av_read_frame(avFmtCtx,packet) >= ){
        //有视频流帧数据、音频流帧数据、字幕流......
        if(packet->stream_index == av_stream_index){
            //我们只需要解码一帧视频压缩数据,得到视频像素数据
            //老的API
            //参数一:解码器上下文
            //参数二:一帧数据
            //参数三:是否正在解码(0:代表解码完毕,非0:正在解码)
            //返回值:小于0解码失败(错误、异常),否则成功解码一帧数据
//            ret = avcodec_decode_video2(avCdCtx,in_frame_picture,&got_picture_ptr,packet);

            //新的API: avcodec_send_packet() and avcodec_receive_frame().
            //avcodec_send_packet: 发送数据包(通俗解释:解压一帧视频压缩数据)
            //avcodec_receive_frame: 接收一帧解析成功之后的视频像素数据
            //第一步:avcodec_send_packet
            //参数一:解码器上下文
            //参数二:数据包(通俗讲解:一帧视频压缩数据)
            //返回值:
            //AVERROR(EAGAIN): 当前数据不可用,你可以尝试继续重新发送一次(通俗讲解:继续解码下一帧)
            //AVERROR_EOF: 解码完成
            //AVERROR(EINVAL): 当前你一个解码器,但是没有打开或者已经关闭了(通俗讲解:不可用)
            //AVERROR(ENOMEM): 当前解码一帧视频压缩数据发送了异常
            avcodec_send_packet(avCdCtx,packet);
//            if(ret != 0 || ret != AVERROR(EAGAIN)){
//                LOG_I("解码失败");
//                return;
//            }
            //第二步:avcodec_receive_frame
            //接收一帧解码视频像素数据
            //返回值和avcodec_send_packet返回值含义相同
            ret = avcodec_receive_frame(avCdCtx,in_frame_picture);

            if(ret == ){
                //接下来我要将解码后的数据(视频像素数据,保存为YUV420P文件)
                //在这个地方需要指定输出文件的类型(格式转换)
                //我要将AVFrame转成视频像素数YUV420P格式
                //参数一:视频像素数据格式上下文(SwsContext)
                //参数二:输入的数据(转格式前的视频像素数据)
                //参数三:输入画面每一行的大小(视频像素数据转换一行一行的转)
                //参数四:输入画面每一行的要转码的开始位置
                //参数五:输出画面数据(转格式后的视频像素数据)
                //参数六:输出画面每一行的大小
                sws_scale(sws_ctx,(const uint8_t *const*)in_frame_picture->data,
                          in_frame_picture->linesize,,avCdCtx->height,
                          out_frame_picture_YUV42P->data,out_frame_picture_YUV42P->linesize);
                //普及: YUV420P格式结构
                //Y代表亮度,UV代表色度(人的眼睛对亮度敏感,对色度不敏感)
                //再深入:计算机图像学相关
                //YUV420P格式规定一:Y结构表示一个像素点(一个像素点就是一个Y)
                //YUV420P格式规定二:四个Y对应一个U和一个V(也就是四个像素点,对应一个U和V)
                //Y默认情况下:灰度
                //计算Y大小:y = 宽x高
                y_size = avCdCtx->width * avCdCtx->height;
                u_size = y_size / ;
                v_size = y_size / ;
                //写入文件
                //首先写入Y,再是U,再是V
                //in_frame_picture->data[0]表示Y
                fwrite(in_frame_picture->data[],,y_size,out_file_yuv);
                //in_frame_picture->data[1]表示U
                fwrite(in_frame_picture->data[],,u_size,out_file_yuv);
                //in_frame_picture->data[2]表示V
                fwrite(in_frame_picture->data[],,v_size,out_file_yuv);
                frame_index++;
                LOG_I_ARGS("当前是第%d帧",frame_index);
            }
        }
        //老的API写法
        //关闭流
//        av_free_packet(packet);
    }
    av_packet_free(&packet);
    //关闭流
    fclose(out_file_yuv);
    av_frame_free(&in_frame_picture);
    av_frame_free(&out_frame_picture_YUV42P);
    avcodec_close(avCdCtx);
    avformat_free_context(avFmtCtx);
}
           

代码地址:CSDN下载地址