FFmpeg 视频解码流程及常用结构体解析 [转]
目录
ffmpeg解码流程
ffmpeg旧接口的解码流程
新接口解码流程
- 注意 在新接口流程中使用avcodec_parameters_to_context函数来初始解码器参数,在未加入该步骤之前解析avi封装的mpeg4视频没问题但是解析MP4封装的mpeg4视频会报如下错误
Picture size is 0x00
- 加上该步骤后解决(解析wmv格式视频也必须加入这一步)
使用到的ffmpeg结构体及API说明
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】
- 方法一
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);
- 方法二
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。
AVCodec结构体
- ffmpeg中的解码器及编码器都用AVCodec结构体保存一些编解码的配置信息。
- 对解码来说可以按照下面方式使用 avcodec_find_decoder 或者 avcodec_find_decoder_by_name。
//解码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);
AVStream结构体
- 该结构体用于描述一个流媒体:
- 该结构体中大部分值域可以由avformat_open_input函数根据文件头的信息确定,缺少的信息需要通过调用av_find_stream_info进一步获得。
- av_find_stream_info函数读取一部分音视频来获取有关视频文件的一些信息,如编码宽高、视频时长等。
- 对于一些没有头部信息的视频文件(如mpeg编码的文件)调用该函数是必须的。
- 调用该函数可能会带了很大的延迟。
- 延迟优化方法参考:https://jiya.io/archives/vlc_optimize_1.html
- 主要的成员域:
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时生成。
AVIOContext结构体
- 用于管理FFMPEG输入输出数据的结构体。
- 主要成员:
unsigned 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。
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
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: 是否是隔行扫描 uint8_t motion_subsample_log2: 一个宏块中的运动矢量采样个数,取log的
解码Demo
#include "ffmpeg_test.h" #include <stdio.h> #include <iostream> #include <string> using namespace std; #define INBUF_SIZE (300000) 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; typedef struct DecodeFrameParams { unsigned int mInputCodecType; unsigned int mOutputPixelFormat; }DecodeFrameParams; bool GetDecodedFrame(MediaFrame& rawFrame, DecodeFrameParams& params) { 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; } if (mImgConvertCtx) { sws_freeContext(mImgConvertCtx); } 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); mImgConvertCtx = sws_getContext(mVideoFrame->width, mVideoFrame->height, mAvContext->pix_fmt, mVideoFrame->width, mVideoFrame->height, strm_to_av_map_pixel_format[params.mOutputPixelFormat], SWS_BICUBIC, NULL, NULL, NULL); sws_scale(mImgConvertCtx, (const unsigned char* const*)mVideoFrame->data, mVideoFrame->linesize, 0, mVideoFrame->height, mFrameYUV->data, mFrameYUV->linesize); //mWidth = mVideoFrame->width; //mHeight = mVideoFrame->height; // 配置输出信息 rawFrame.frameBuf = (unsigned char*)buf_ptr; rawFrame.frameSize = frameSize; return true; } 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(); mVideoFrame = av_frame_alloc(); mFrameYUV = av_frame_alloc(); bool mHasKeyFrame = false; AVPacket mPktPacket; long long mDecodedBytes = 0; int ret = avformat_open_input(&mFormatCtx, filename.c_str(), NULL, NULL); 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) { 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]); mAvContext = avcodec_alloc_context3(mVcodec); if (!mVcodec || !mAvContext) { //DecoderDeinit(); return false; } ///不初始化解码器context会导致MP4封装的mpeg4码流解码失败 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) { //DecoderDeinit(); return false; } //循环读入H264数据 AVPacket h264Pack; while (1) { av_init_packet(&h264Pack); int ret = av_read_frame(mFormatCtx, &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; } mHasKeyFrame = true; if (mHasKeyFrame) { // 初始化待解码包 av_init_packet(&mPktPacket); mPktPacket.data = (uint8_t*)h264Pack.data; // 用于解码的压缩视频帧数据,mBitStreamBuf中包含pps数据 mPktPacket.size = h264Pack.size; mPktPacket.pts = h264Pack.pts; mPktPacket.dts = h264Pack.dts; mDecodedBytes += mPktPacket.size; // 发送待解码包 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; } //error = "receive frame failed"; // 输出数据接收失败 //mHasKeyFrame = false; //return false; } DecodeFrameParams params; params.mInputCodecType = mInputCodecType; params.mOutputPixelFormat = mOutputPixelFormat; MediaFrame rawFrame; // 获取解码后的YUV数据 GetDecodedFrame(rawFrame, params); //int mFrameRate = mAvContext->framerate.num; // 写文件保存视频数据 fwrite(rawFrame.frameBuf, rawFrame.frameSize, 1, fp_YUV); fflush(fp_YUV); if (rawFrame.frameBuf) { delete[] rawFrame.frameBuf; rawFrame.frameBuf = NULL; } } } fclose(fp_YUV); avformat_close_input(&mFormatCtx); //printf("[strmsdk] decoder[%s] open successful.\n", mVcodec->name); } int flush_encoder(AVFormatContext *fmt_ctx, unsigned int stream_index) { int ret; int got_frame; AVPacket enc_pkt; if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities & AV_CODEC_CAP_DELAY)) { return 0; } while (1) { enc_pkt.data = NULL; enc_pkt.size = 0; av_init_packet(&enc_pkt); ret = avcodec_encode_video2(fmt_ctx->streams[stream_index]->codec, &enc_pkt, NULL, &got_frame); av_frame_free(NULL); if (ret < 0) break; if (!got_frame) { ret = 0; break; } printf("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n", enc_pkt.size); /* mux encoded frame */ ret = av_write_frame(fmt_ctx, &enc_pkt); if (ret < 0) break; } return ret; } int test_encode(int argc, char* argv[]) { AVFormatContext* pFormatCtx; AVOutputFormat* fmt; AVStream* video_st; AVCodecContext* pCodecCtx; AVCodec* pCodec; AVPacket pkt; uint8_t* picture_buf; AVFrame* pFrame; int picture_size; int y_size; int framecnt = 0; //FILE *in_file = fopen("src01_480x272.yuv", "rb"); //Input raw YUV data FILE *in_file = fopen("ds_352x288.yuv", "rb"); //Input raw YUV data int in_w = 352, in_h = 288; //Input data's width and height int framenum = 50; //Frames to encode //const char* out_file = "src01.h264"; //Output Filepath //const char* out_file = "src01.ts"; //const char* out_file = "src01.hevc"; const char* out_file = "ds.h264"; av_register_all(); //Method1. pFormatCtx = avformat_alloc_context(); //Guess Format fmt = av_guess_format(NULL, out_file, NULL); pFormatCtx->oformat = fmt; //Method 2. //avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, out_file); //fmt = pFormatCtx->oformat; //Open output URL if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0) { printf("Failed to open output file! \n"); return -1; } video_st = avformat_new_stream(pFormatCtx, 0); //video_st->time_base.num = 1; //video_st->time_base.den = 25; if (video_st == NULL) { return -1; } //Param that must set pCodecCtx = video_st->codec; //pCodecCtx->codec_id =AV_CODEC_ID_HEVC; pCodecCtx->codec_id = fmt->video_codec; pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO; pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P; pCodecCtx->width = in_w; pCodecCtx->height = in_h; pCodecCtx->bit_rate = 400000; pCodecCtx->gop_size = 25; //I帧间隔 pCodecCtx->time_base.num = 1; pCodecCtx->time_base.den = 25; //time_base一般是帧率的倒数,但不总是 pCodecCtx->framerate.num = 25; //帧率 pCodecCtx->framerate.den = 1; ///AVFormatContext* mFormatCtx ///mBitRate = mFormatCtx->bit_rate; ///码率存储位置 ///mFrameRate = mFormatCtx->streams[stream_id]->avg_frame_rate.num; //H264 //pCodecCtx->me_range = 16; //pCodecCtx->max_qdiff = 4; //pCodecCtx->qcompress = 0.6; pCodecCtx->qmin = 10; pCodecCtx->qmax = 51; //Optional Param pCodecCtx->max_b_frames = 0; //不要B帧 // Set Option AVDictionary *param = 0; //H.264 if (pCodecCtx->codec_id == AV_CODEC_ID_H264) { av_dict_set(¶m, "preset", "slow", 0); av_dict_set(¶m, "tune", "zerolatency", 0); //av_dict_set(¶m, "profile", "main", 0); } //H.265 if (pCodecCtx->codec_id == AV_CODEC_ID_H265) { av_dict_set(¶m, "preset", "ultrafast", 0); av_dict_set(¶m, "tune", "zero-latency", 0); } //Show some Information av_dump_format(pFormatCtx, 0, out_file, 1); pCodec = avcodec_find_encoder(pCodecCtx->codec_id); if (!pCodec) { printf("Can not find encoder! \n"); return -1; } if (avcodec_open2(pCodecCtx, pCodec, ¶m) < 0) { printf("Failed to open encoder! \n"); return -1; } pFrame = av_frame_alloc(); picture_size = av_image_get_buffer_size(pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, 1); picture_buf = (uint8_t *)av_malloc(picture_size); //avpicture_fill((AVPicture *)pFrame, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height); av_image_fill_arrays(pFrame->data, pFrame->linesize, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, 1); //Write File Header avformat_write_header(pFormatCtx, NULL); av_new_packet(&pkt, picture_size); y_size = pCodecCtx->width * pCodecCtx->height; for (int i = 0; i < framenum; i++) { int ret = 0; //Read raw YUV data if (fread(picture_buf, 1, y_size * 3 / 2, in_file) <= 0) { printf("Failed to read raw data! \n"); return -1; } else if (feof(in_file)) { break; } int got_picture = 0; pFrame->data[0] = picture_buf; // Y pFrame->data[1] = picture_buf + y_size; // U pFrame->data[2] = picture_buf + y_size * 5 / 4; // V //PTS //pFrame->pts=i; pFrame->pts = i; pFrame->width = pCodecCtx->width; pFrame->height = pCodecCtx->height; pFrame->format = AV_PIX_FMT_YUV420P; //Encode 旧版接口 /*ret = avcodec_encode_video2(pCodecCtx, &pkt, pFrame, &got_picture); if (ret < 0) { printf("Failed to encode! \n"); return -1; } if (got_picture == 1) { printf("Succeed to encode frame: %5d\tsize:%5d\n", framecnt, pkt.size); framecnt++; pkt.stream_index = video_st->index; ret = av_write_frame(pFormatCtx, &pkt); av_free_packet(&pkt); }*/ ret = avcodec_send_frame(pCodecCtx, pFrame); if (ret < 0) { fprintf(stderr, "Error sending a frame for encoding\n"); return -1; } while (ret == 0) { //framecnt++; ret = avcodec_receive_packet(pCodecCtx, &pkt); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { framecnt++; break; } //framecnt++; pkt.stream_index = video_st->index; ret = av_write_frame(pFormatCtx, &pkt); av_packet_unref(&pkt); } } //Flush Encoder int ret = flush_encoder(pFormatCtx, 0); if (ret < 0) { printf("Flushing encoder failed\n"); return -1; } //Write file trailer av_write_trailer(pFormatCtx); //Clean if (video_st) { avcodec_close(video_st->codec); av_free(pFrame); av_free(picture_buf); } avio_close(pFormatCtx->pb); avformat_free_context(pFormatCtx); fclose(in_file); return 0; }