ffmpeg解码流程

1、ffmpeg解码流程

《1》、ffmpeg旧接口的解码流程

 

《2》、新接口解码流程

 

  注意在新接口流程中使用avcodec_parameters_to_context函数来初始解码器参数,在未加入该步骤之前解析avi封装的mpeg4视频没问题但是解析MP4封装的mpeg4视频会报如下错误

Picture size is 0x00

1加上该步骤后解决(解析wmv格式视频也必须加入这一步)

2、使用到的ffmpeg结构体及API说明

《1》、AVFormatContext结构体

  该结构体描述了一个媒体文件或媒体流的构成和基本信息。它是一个贯穿始终的数据结构,很多函数调用需要使用到它。它也是FFMPEG解封装(flv,avi,mp4)功能的结构体。

其主要的几个变量(主要考虑解码情况):

struct AVInputFormat *iformat;

//输入数据的封装格式。仅解封装用,由avformat_open_input()设置。

struct AVOutputFormat *oformat;

//输出数据的封装格式。仅封装用,调用者在avformat_write_header()之前设置。

AVIOContext *pb;// I/O上下文。

解封装:由用户在avformat_open_input()之前设置(然后用户必须手动关闭它)或通过avformat_open_input()设置。

封装:由用户在avformat_write_header()之前设置。 调用者必须注意关闭/释放IO上下文。

unsigned int nb_streams;//AVFormatContext.streams中元素的个数。

AVStream **streams;//文件中所有流的列表。char filename[1024];//输入输出文件名。

int64_t start_time;//第一帧的位置。

int64_t duration;//流的持续时间

int64_t bit_rate;//总流比特率(bit / s),如果不可用则为0。

int64_t probesize;

//从输入读取的用于确定输入容器格式的数据的最大大小。

仅封装用,由调用者在avformat_open_input()之前设置。

AVDictionary *metadata;//元数据

AVCodec *video_codec;//视频编解码器

AVCodec *audio_codec;//音频编解码器

AVCodec *subtitle_codec;//字母编解码器

AVCodec *data_codec;//数据编解码器

int (*io_open)(struct AVFormatContext *s, AVIOContext **pb, const char *url, int flags, AVDictionary **options);

//打开IO stream的回调函数。

void (*io_close)(struct AVFormatContext *s, AVIOContext *pb);

//关闭使用AVFormatContext.io_open()打开的流的回调函数。

使用时可以通过avformat_alloc_context分配后使用,也可以直接avformat_open_input。

//1、方法一

AVFormatContext *fmt_ctx = NULL;

string filename = "test.avi" ;

fmt_ctx = avformat_alloc_context();

avformat_open_input(&fmt_ctx, ilename.c_str(), NULL, NULL);

avformat_close_input(&fmt_ctx);

//2、方法二

AVFormatContext *fmt_ctx = NULL;

string filename = "test.avi" ;

int ret = avformat_open_input(&fmt_ctx, filename.c_str(), NULL, NULL);

avformat_close_input(&fmt_ctx);

推荐使用方法2,因为若传进avformat_open_input的fmt_ctx为NULL,该函数内部会调用avformat_alloc_context函数。相应的avformat_close_input内部会调用avformat_free_context。

《2》、AVCodec

  ffmpeg中的解码器及编码器都用AVCodec结构体保存一些编解码的配置信息。
  对解码来说可以按照下面方式使用

```

  //解码H264流 

   AVCodec*   Vcodec = NULL;

  Vcodec = avcodec_find_decoder(AV_CODEC_ID_H264); 

  //或者直接通过解码器名字找到解码器

  Vcodec = avcodec_find_decoder_by_name("h264_mediacodec");

  ```

AVCodecContext

  该结构体用于存储编解码器上下文的数据结构,包含了众多编解码需要的参数信息。这些信息参数需要进行初始化,使用avcodec_parameters_to_context进行初始化。不初始化解析一些格式的封装视频会导致编解码失败。该结构体内很多参数是编码时使用的,解码用不上。

几个主要的成员:

enum AVMediaType codec_type:编解码器的类型(视频,音频...)

struct AVCodec  *codec:采用的解码器AVCodec(H.264,MPEG2...)

int bit_rate:平均比特率

uint8_t *extradata; int extradata_size:针对特定编码器包含的附加信息(例如对于H.264解码器来说,存储SPS,PPS等)

AVRational time_base:根据该参数,可以把PTS转化为实际的时间(单位为秒s)

int width, height:如果是视频的话,代表宽和高

int refs:运动估计参考帧的个数(H.264的话会有多帧,MPEG2这类的一般就没有了)

int sample_rate:采样率(音频)

int channels:声道数(音频)

enum AVSampleFormat sample_fmt:采样格式

int profile:型(H.264里面就有,其他编码标准应该也有)

int level:级(和profile差不太多)

使用:

AVCodec*   Vcodec = NULL;

Vcodec = avcodec_find_decoder(AV_CODEC_ID_H264);

AVCodecContext*     AvContext = NULL;

AvContext = avcodec_alloc_context3(mVcodec);

avcodec_parameters_to_context(mAvContext,

fmt_ctx->streams[mVideoStreamIdx]->codecpar);

《4》、AVStream

  该结构体用于描述一个流媒体,该结构体中大部分值域可以由avformat_open_input函数根据文件头的信息确定,缺少的信息需要通过调用av_find_stream_info进一步获得。

av_find_stream_info函数读取一部分音视频来获取有关视频文件的一些信息,如编码宽高、视频时长等。对于一些没有头部信息的视频文件(如mpeg编码的文件)调用该函数是必须的。调用该函数可能会带了很大的延迟。

延迟优化方法参考。

主要的成员域:

index/id:index对应流的索引,这个数字是自动生成的,根据index可以从AVFormatContext::streams表中索引到该流;而id则是流的标识,依赖于具体的容器格式。比如对于MPEG TS格式,id就是pid。

time_base:流的时间基准,是一个实数,该流中媒体数据的pts和dts都将以这个时间基准为粒度。通常,使用av_rescale/av_rescale_q可以实现不同时间基准的转换。

start_time:流的起始时间,以流的时间基准为单位,通常是该流中第一个帧的pts。

duration:流的总时间,以流的时间基准为单位。

need_parsing:对该流parsing过程的控制域。

nb_frames:流内的帧数目。

avg_frame_rate:帧率相关。

codec:指向该流对应的AVCodecContext结构,调用avformat_open_input时生成。

parser:指向该流对应的AVCodecParserContext结构,调用av_find_stream_info时生成。

《5》、AVIOContext

  用于管理FFMPEG输入输出数据的结构体。

主要成员:

nsigned char *buffer:缓存开始位置

int buffer_size:缓存大小(默认32768)

unsigned char *buf_ptr:当前指针读取到的位置

unsigned char *buf_end:缓存结束的位置

void *opaque:URLContext结构体

在解码的情况下,buffer用于存储ffmpeg读入的数据。如打开一个视频文件时,先把数据从硬盘读入buffer,然后在送给解码器解码。
  opaque按照这篇博客说是指向URLContext,找源码没有找到相关的赋值操作但是在aviobuf.c的下面这个函数找到佐证。

*

typedef struct AVIOInternal {

    URLContext *h;

} AVIOInternal;

*/

static void *ff_avio_child_next(void *obj, void *prev)

{

    AVIOContext *s = obj;

    AVIOInternal *internal = s->opaque;

    return prev ? NULL : internal->h;

}

URLContext结构体中有一个URLProtocol。每种协议(rtp,rtmp,file,udp等)都有一个对应的URLProtocol。

《6》、AVPacket

  该结构体是ffmpeg中很重要的一个结构体,它保存了解码后或编码前的数据(仍然是压缩数据)和这些数据的一些附加信息,如显示时间戳(pts)、数据时长、所在媒体的索引等。

对于视频来说,一个AVPacket通常包含一帧压缩数据,而音频则有可能包含多个压缩的Frame。

重要的成员变量:

uint8_t *data:压缩编码的数据。

例如对于H.264来说。1个AVPacket的data通常对应一个NAL。

注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流

因此在使用FFMPEG进行视音频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。

int size:data的大小

int64_t pts:显示时间戳

int64_t dts:解码时间戳

int stream_index:标识该AVPacket所属的视频/音频流。

avpacket.h内有API说明,常用的几个API

av_packet_ref,av_packet_unref

av_new_packet, av_packet_alloc, av_init_packet, av_packet_unref,av_packet_free(free这个API为旧接口)

av_packet_clone:拷贝packet

《7》、AVFrame

  AVFrame结构体一般用于存储原始数据(非压缩的YUV,RGB数据等),此外还包含一些相关信息,比如解码的时候存储宏块类型表,QP表,运动矢量等数据。

AVFrame必须用av_frame_alloc分配,用av_frame_free释放。注意av_frame_alloc函数只创建实例但是该实例存储数据的buffer则需要通过另外的操作进行分配,如av_image_fill_arrays接口。

AVFrame内部几个常用的成员:

uint8_t *data[AV_NUM_DATA_POINTERS]:解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM)

int linesize[AV_NUM_DATA_POINTERS]:data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽。

int width, height:视频帧宽和高(1920x1080,1280x720…)

int nb_samples:音频的一个AVFrame中可能包含多个音频帧,在此标记包含了几个

int format:解码后原始数据类型(YUV420,YUV422,RGB24…)

int key_frame:是否是关键帧

enum AVPictureType pict_type:帧类型(I,B,P…)

AVRational sample_aspect_ratio:宽高比(16:9,4:3…)

int64_t pts:显示时间戳

int coded_picture_number:编码帧序号

int display_picture_number:显示帧序号

int interlaced_frame:是否是隔行扫描

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

解码demo

AVCodecID strm_to_av_map_coding_type[STRM_CODEC_MAX] =

{

    AV_CODEC_ID_NONE,          //0

    AV_CODEC_ID_H264,          //1

    AV_CODEC_ID_H265,          //2

    AV_CODEC_ID_MPEG1VIDEO,    //3

    AV_CODEC_ID_MPEG2VIDEO,    //4

    AV_CODEC_ID_MJPEG,         //5

    AV_CODEC_ID_MPEG4          //6

 

}

AVPixelFormat strm_to_av_map_pixel_format[STRM_PIXFMT_MAX] =

{

    AV_PIX_FMT_NONE,

    AV_PIX_FMT_RGB24,

    AV_PIX_FMT_RGBA,

    AV_PIX_FMT_RGB565,

    AV_PIX_FMT_ARGB,

    AV_PIX_FMT_YUV420P,

    AV_PIX_FMT_YUV422P,

    AV_PIX_FMT_YUV444P,

    AV_PIX_FMT_GRAY8,

    AV_PIX_FMT_NV21,

    AV_PIX_FMT_UYVY422,

    AV_PIX_FMT_YUYV422

};

static AVFrame*    mVideoFrame = NULL;

static AVFrame*      mFrameYUV = NULL;

// 颜色转换上下文

static struct SwsContext*  mImgConvertCtx = NULL;

///< 解码器

static AVCodec*            mVcodec = NULL;

///< 解码上下文

static AVCodecContext*     mAvContext = NULL;

static AVFormatContext*    mFormatCtx = NULL;  // 用于读取文件  

static int mVideoStreamIdx = 0;

 

int test_decode(unsigned int mInputCodecType, unsigned int mOutputPixelFormat,

    string &filename, string &error)

{

    FILE* fp_YUV = fopen("decode_out.yuv", "wb+");

    if (!fp_YUV)

    {   perror("open out.yuv :");

        return -1;

    }

    av_register_all();                                        //1 、初始化

    mVideoFrame = av_frame_alloc();

    mFrameYUV   = av_frame_alloc();

    bool                mHasKeyFrame = false;

    AVPacket            mPktPacket;

    int ret = avformat_open_input(&mFormatCtx, filename.c_str(), NULL, NULL);     //2 、打开文件

    if (ret < 0)

    {

        printf("avformat_open_input fail \n");

        error = "avformat_open_input fail";

        av_log(NULL, AV_LOG_ERROR, "Cannot open file: %s.\n", filename.c_str());

        return false;

    }

    if (avformat_find_stream_info(mFormatCtx, NULL) < 0)               //3、找到视频流

    {

        return false;

    }

    av_dump_format(mFormatCtx, 0, filename.c_str(), 0);    

    //获取视频的编码信息  

    for (uint32_t i = 0; i < mFormatCtx->nb_streams; ++i)

    {

        if (mFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)

        {

            mVideoStreamIdx = i;

            break;

        }

    }

    // 寻找解码器

    mVcodec = avcodec_find_decoder(strm_to_av_map_coding_type[mInputCodecType]);  //4、根据视频流找到编码器

    mAvContext = avcodec_alloc_context3(mVcodec);                        //5、为编码器分配内存

    if (!mVcodec || !mAvContext)

    {

        return false;

    }

    ///不初始化解码器context会导致MP4封装的mpeg4码流解码失败        //6、完成数据的拷贝

    ret = avcodec_parameters_to_context(mAvContext, mFormatCtx->streams[mVideoStreamIdx]->codecpar);

    if (ret < 0)

    {

        av_log(NULL, AV_LOG_ERROR, "Error initializing the decoder context.\n");

        //exit_program(1);

    }

    // 打开解码器

    mAvContext->pix_fmt = strm_to_av_map_pixel_format[mOutputPixelFormat];   

    if (avcodec_open2(mAvContext, mVcodec, NULL) != 0)                        //7、打开编码器

    {

        //DecoderDeinit();

        return false;

    }

    //循环读入H264数据 

    AVPacket h264Pack;                                               //定义一个 AVPacket

    while (1)

    {

        av_init_packet(&h264Pack);                                   //初始化

        int ret = av_read_frame(mFormatCtx, &h264Pack);           //读取一帧放入  h264Pack 中

        if (ret != 0)

        {

            error = "Read a frame failed";

            av_packet_unref(&h264Pack);

            return false;

        }

        else if (h264Pack.stream_index != mVideoStreamIdx)

        {

            // not a video packet, skip it in this version

            av_packet_unref(&h264Pack);

            continue;

        }

        {

            // 初始化待解码包

            av_init_packet(&mPktPacket);

            mPktPacket.data = (uint8_t*)h264Pack.data;     // 用于解码的压缩视频帧数据

            mPktPacket.size = h264Pack.size;               //封装数据

            mPktPacket.pts = h264Pack.pts;

            mPktPacket.dts = h264Pack.dts;

            // 发送待解码包

            if (avcodec_send_packet(mAvContext, &mPktPacket))

            {

                error = "send packet failed";

                mHasKeyFrame = false;

                av_packet_unref(&mPktPacket);

                return false;

            }

 

            av_packet_unref(&mPktPacket);

 

            // 接收解码数据

            int ret = avcodec_receive_frame(mAvContext, mVideoFrame);

            if (ret != 0)

            {

                if (ret == AVERROR(EAGAIN))

                {

                    // 暂时没有输出,需要更多输入

                    error = "need more data";

                    //return false;

                    continue;

                }

            }

            DecodeFrameParams params;

            params.mInputCodecType = mInputCodecType;

            params.mOutputPixelFormat = mOutputPixelFormat;

            int frameSize = av_image_get_buffer_size(strm_to_av_map_pixel_format[params.mOutputPixelFormat],

mVideoFrame->width, mVideoFrame->height, 1);

 void* buf_ptr = NULL;

    buf_ptr = (void*)new(std::nothrow) char[frameSize];

    if (buf_ptr == NULL)

    {

        printf("decode memory allocation error! [buf_size = %d]\n", frameSize);

        return false;

    }

        av_image_fill_arrays(mFrameYUV->data, mFrameYUV->linesize, (uint8_t*)buf_ptr,

        strm_to_av_map_pixel_format[params.mOutputPixelFormat], mVideoFrame->width,

        mVideoFrame->height, 1);

            // 写文件保存视频数据

            fwrite(buf_ptr, frameSize, 1, fp_YUV);

            fflush(fp_YUV);

            if(buf_ptr)

            {

                delete [] buf_ptr;

                buf_ptr = NULL;

            }           

        }       

    }

    fclose(fp_YUV);

    avformat_close_input(&mFormatCtx);

    //printf("[strmsdk] decoder[%s] open successful.\n", mVcodec->name);

}

int main()

{

    //解码demo -> YUV

    string error;

    string filename;

    cout << "Input video: " << endl;

    cin >> filename;

 

    test_decode(6, 5, filename, error);

    cout << "error: " << error << endl;

 

}

上一篇:流的基本概念以及文件专属的四个流(1)


下一篇:io笔记(二)