FFmpeg视频解码流程详解及demo

        本文主要讲解FFmpeg的视频解码具体流程,API使用,以及相关结构体介绍。最后再以一个非常简单的demo演示将一个mp4格式的视频文件解码为原始数据yuv文件。

        FFmpeg解码流程有2套API接口,本文主要围绕新接口。往期也有写过两篇Android平台上使用FFmpeg新旧接口解码并播放的简单demo的介绍,可以参考:

        1、FFmpeg_Android纯视频播放demo--基于旧接口

        2、FFmpeg_Android纯视频播放demo2--基于新接口

一、解码流程使用到的FFmpeg结构体说明

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");

3、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输入输出数据的结构体。

        主要成员:

unsigned char *buffer:缓存开始位置

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

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

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

void *opaque:URLContext结构体

        在解码的情况下,buffer用于存储ffmpeg读入的数据。如打开一个视频文件时,先把数据从硬盘读入buffer,然后在送给解码器解码。

        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。

        几个常用变量:

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的

二、解码过程的API调用流程

1、API调用流程图:

        这里主要针对解码新接口:

 2、解码并生成YUV文件过程API调用说明

(1)、注册各大组件

    //注册各大组件
    av_register_all();

(2)、打开视频文件并获取相关上下文

        在解码之前我们得获取里面的内容,这一步就是打开地址并且获取里面的内容。其中avFormatContext是内容的一个上下文。

        并使用avformat_open_input打开播放源,inputPath为输入的地址,可以是视频文件,也可以是网络视频流。然后使用avformat_find_stream_info从获取的内容中寻找相关流。

    AVFormatContext *avFormatContext = avformat_alloc_context();    //获取上下文
    //打开视频地址并获取里面的内容(解封装)
    if (avformat_open_input(&avFormatContext, inputPath, NULL, NULL) < 0) {
        LOGE("打开视频失败")
        return;
    }
    if (avformat_find_stream_info(avFormatContext, NULL) < 0) {
        LOGE("获取内容失败")
        return;
    }

(3)、寻找视频流

        我们在上面已经获取了内容,但是在一个音视频中包括了音频流,视频流和字幕流,所以在所有的内容当中,我们应当找出相对应的视频流。

    //获取视频的编码信息
    AVCodecParameters *origin_par = NULL;
    int mVideoStreamIdx = -1;
    mVideoStreamIdx = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (mVideoStreamIdx < 0) {
        av_log(NULL, AV_LOG_ERROR, "Can't find video stream in input file\n");
        return;
    }
    LOGE("成功找到视频流")

(4)、获取并打开解码器

        如果要进行解码,那么得有解码器并打开解码器。avcodec_parameters_to_context去初始化解码器否则解析avi封装的mpeg4视频没问题但是解析MP4封装的mpeg4视频会报错。如下:

    // 寻找解码器 {start
    AVCodec *mVcodec = NULL;
    AVCodecContext *mAvContext = NULL;
    mVcodec = avcodec_find_decoder(origin_par->codec_id);
    mAvContext = avcodec_alloc_context3(mVcodec);
    if (!mVcodec || !mAvContext) {
        return;
    }
 
 
    //不初始化解码器context会导致MP4封装的mpeg4码流解码失败
    int ret = avcodec_parameters_to_context(mAvContext, origin_par);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Error initializing the decoder context.\n");
    }
 
    // 打开解码器
    if (avcodec_open2(mAvContext, mVcodec, NULL) != 0){
        LOGE("打开失败")
        return;
    }
    LOGE("解码器打开成功")
    // 寻找解码器 end}

(5)、申请AVPacket和AVFrame以及相关设置

        申请AVPacket和AVFrame,其中AVPacket的作用是:保存解码之前的数据和一些附加信息,如显示时间戳(pts)、解码时间戳(dts)、数据时长,所在媒体流的索引等;AVFrame的作用是:存放解码过后的数据。

  //申请AVPacket
    AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
    av_init_packet(packet);
    //申请AVFrame
    AVFrame *frame = av_frame_alloc();//分配一个AVFrame结构体,AVFrame结构体一般用于存储原始数据,指向解码后的原始帧

(6)、申请用于存放解码后YUV格式数据的相关buf

        解码后的数据,按照YUV相关格式保存为yuv文件,先申请buf,用于存放解码后的数据,并按照yuv格式排列保存:

     uint8_t *byte_buffer = NULL;

    int byte_buffer_size = av_image_get_buffer_size(mAvContext->pix_fmt, mAvContext->width, mAvContext->height, 32);
    LOGE("width = %d , height = %d ",mAvContext->width, mAvContext->height);
    byte_buffer = (uint8_t*)av_malloc(byte_buffer_size);
    if (!byte_buffer) {
        av_log(NULL, AV_LOG_ERROR, "Can't allocate buffer\n");
        return AVERROR(ENOMEM);
    }

(7)、开始解码

        接下来就可以开始解码,如下是解码的核心段代码:

    // 发送待解码包
    int result = avcodec_send_packet(mAvContext, packet);
    av_packet_unref(packet);
    if (result < 0) {
        av_log(NULL, AV_LOG_ERROR, "Error submitting a packet for decoding\n");
        continue;
    }
 
    // 接收解码数据
    while (result >= 0) {
        result = avcodec_receive_frame(mAvContext, frame);
        if (result == AVERROR_EOF)
             break;
        else if (result == AVERROR(EAGAIN)) {
             result = 0;
             break;
        } else if (result < 0) {
             av_log(NULL, AV_LOG_ERROR, "Error decoding frame\n");
             av_frame_unref(frame);
             break;
        }
        av_frame_unref(frame);
    }

(8)、解码并保存为yuv格式文件

        YUV是解码后的纯视频原数据格式,我们将其保存为yuv文件,用于分析解码后的数据是否正确。完整的解码,赋值,然后保存文件代码如下:

    while(1)
    {
        int ret = av_read_frame(avFormatContext, packet);
        if (ret != 0){
            av_strerror(ret,buf,sizeof(buf));
            LOGE("--%s--\n",buf);
            av_packet_unref(packet);
            break;
        }

        if (ret >= 0 && packet->stream_index != mVideoStreamIdx) {
            av_packet_unref(packet);
            continue;
        }

        {
            // 发送待解码包
            int result = avcodec_send_packet(mAvContext, packet);
            av_packet_unref(packet);
            if (result < 0) {
                av_log(NULL, AV_LOG_ERROR, "Error submitting a packet for decoding\n");
                continue;
            }

            // 接收解码数据
            while (result >= 0){
                result = avcodec_receive_frame(mAvContext, frame);
                if (result == AVERROR_EOF)
                    break;
                else if (result == AVERROR(EAGAIN)) {
                    result = 0;
                    break;
                } else if (result < 0) {
                    av_log(NULL, AV_LOG_ERROR, "Error decoding frame\n");
                    av_frame_unref(frame);
                    break;
                }

                int number_of_written_bytes = av_image_copy_to_buffer(byte_buffer, byte_buffer_size,
                                                                      (const uint8_t* const *)frame->data, (const int*) frame->linesize,
                                                                      mAvContext->pix_fmt, mAvContext->width, mAvContext->height, 1);
                if (number_of_written_bytes < 0) {
                    av_log(NULL, AV_LOG_ERROR, "Can't copy image to buffer\n");
                    av_frame_unref(frame);
                    continue;
                }

                // 写文件保存视频数据
                fwrite(byte_buffer, number_of_written_bytes, 1, fp_YUV);
                fflush(fp_YUV);

                av_frame_unref(frame);
            }
        }

    }

(9)、收尾释放资源

        完成过后记得释放资源。

    //释放
    fclose(fp_YUV);
    av_frame_free(&frame);
    avcodec_close(mAvContext);
    avformat_free_context(avFormatContext);

        至此,mp4格式的视频文件,就被解码出纯视频原始数据并保存为yuv格式文件。

三、demo运行

        demo中指定了视频源文件是/sdcard/input.mp4,编码后的yuv文件是/sdcard/videoDecodeOut.yuv,如下代码,若要改文件,可以在此处修改:

@Override
public void onClick(View view) {
	runOnUiThread(new Runnable() {
		@Override
		public void run() {
			String PATH = Environment.getExternalStorageDirectory().getPath();
			//视频解码
			String input = PATH + File.separator + "input.mp4";
			String output = PATH + File.separator + "videoDecodeOut.yuv";
			decode_test(input,1,output);
		}
	});
}

        运行后截图如下:

         界面比较随意,就没有过多设计,点击"START DECODE"按钮,开始对视频文件进行解码,界面不会有什么提示,等解码完成后会弹出提示词 "解码完成,请自行从手机中拉取yuv文件"

如下:

        我们从运行log中也能看到一些信息,如下:

         当有--End of file--打印出来说明已经解码完成,另外这里有个重要的信息就是width和height,若你不知道原视频的分辨率,可以从这里获取,用于待会的yuv分析需要。

        我们通过adb也能从手机中看到解码后的文件,如下:

        同时我们也能看到解码后的数据文件比原来文件大了很多。

四、分析YUV文件

        我们可以通过adb命令拉取yuv文件到电脑上:

         然后通过YUV播放工具来播放看看yuv文件数据是否正确,yuv播放工具我用的是这个yuvplayer:

         打开它,并设置分辨率(刚才获取到的width和height),和yuv格式(一般是yuv420):

        分辨率没有的话,就选择自定义:

 

         然后在文件那里打开刚才的yuv文件:

        打开后就可以按空格键播放,若yuv数据正确,就可以一帧一帧播放了:

         完整例子已经放到github上,如下:

https://github.com/weekend-y/FFmpeg_Android_Demo/tree/master/demo4_videoDecode

        

猜你喜欢

转载自blog.csdn.net/weekend_y45/article/details/125168344