当前位置:   article > 正文

FFmpeg视频解码流程详解及demo_ffmpeg av_find_best_stream

ffmpeg av_find_best_stream

        本文主要讲解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)功能的结构体。

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

  1. struct AVInputFormat *iformat;//输入数据的封装格式。仅解封装用,由avformat_open_input()设置。
  2. struct AVOutputFormat *oformat;//输出数据的封装格式。仅封装用,调用者在avformat_write_header()之前设置。
  3. AVIOContext *pb;// I/O上下文。
  4. 解封装:由用户在avformat_open_input()之前设置(然后用户必须手动关闭它)或通过avformat_open_input()设置。
  5. 封装:由用户在avformat_write_header()之前设置。 调用者必须注意关闭/释放IO上下文。
  6. unsigned int nb_streams;//AVFormatContext.streams中元素的个数。
  7. AVStream **streams;//文件中所有流的列表。char filename[1024];//输入输出文件名。
  8. int64_t start_time;//第一帧的位置。
  9. int64_t duration;//流的持续时间
  10. int64_t bit_rate;//总流比特率(bit / s),如果不可用则为0。
  11. int64_t probesize;
  12. //从输入读取的用于确定输入容器格式的数据的最大大小。
  13. 仅封装用,由调用者在avformat_open_input()之前设置。
  14. AVDictionary *metadata;//元数据
  15. AVCodec *video_codec;//视频编解码器
  16. AVCodec *audio_codec;//音频编解码器
  17. AVCodec *subtitle_codec;//字母编解码器
  18. AVCodec *data_codec;//数据编解码器
  19. int (*io_open)(struct AVFormatContext *s, AVIOContext **pb, const char *url, int flags, AVDictionary **options);
  20. //打开IO stream的回调函数。
  21. void (*io_close)(struct AVFormatContext *s, AVIOContext *pb);
  22. //关闭使用AVFormatContext.io_open()打开的流的回调函数。

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

  1. //1、方法一
  2. AVFormatContext *fmt_ctx = NULL;
  3. string filename = "test.avi" ;
  4. fmt_ctx = avformat_alloc_context();
  5. avformat_open_input(&fmt_ctx, ilename.c_str(), NULL, NULL);
  6. avformat_close_input(&fmt_ctx);
  7. //2、方法二
  8. AVFormatContext *fmt_ctx = NULL;
  9. string filename = "test.avi" ;
  10. int ret = avformat_open_input(&fmt_ctx, filename.c_str(), NULL, NULL);
  11. 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结构体保存一些编解码的配置信息。

        对解码来说可以按照下面方式使用。

  1. //解码H264流
  2. AVCodec* Vcodec = NULL;
  3. Vcodec = avcodec_find_decoder(AV_CODEC_ID_H264);
  4. //或者直接通过解码器名字找到解码器
  5. Vcodec = avcodec_find_decoder_by_name("h264_mediacodec");

3、AVCodecContext

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

        几个主要的成员:

  1. enum AVMediaType codec_type:编解码器的类型(视频,音频...)
  2. struct AVCodec *codec:采用的解码器AVCodec(H.264,MPEG2...)
  3. int bit_rate:平均比特率
  4. uint8_t *extradata; int extradata_size:针对特定编码器包含的附加信息(例如对于H.264解码器来说,存储SPS,PPS等)
  5. AVRational time_base:根据该参数,可以把PTS转化为实际的时间(单位为秒s)
  6. int width, height:如果是视频的话,代表宽和高
  7. int refs:运动估计参考帧的个数(H.264的话会有多帧,MPEG2这类的一般就没有了)
  8. int sample_rate:采样率(音频)
  9. int channels:声道数(音频)
  10. enum AVSampleFormat sample_fmt:采样格式
  11. int profile:型(H.264里面就有,其他编码标准应该也有)
  12. int level:级(和profile差不太多)

        使用:

  1. AVCodec* Vcodec = NULL;
  2. Vcodec = avcodec_find_decoder(AV_CODEC_ID_H264);
  3. AVCodecContext* AvContext = NULL;
  4. AvContext = avcodec_alloc_context3(mVcodec);
  5. avcodec_parameters_to_context(mAvContext,
  6. fmt_ctx->streams[mVideoStreamIdx]->codecpar);

4、AVStream

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

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

        主要的成员:

  1. index/id:index对应流的索引,这个数字是自动生成的,根据index可以从AVFormatContext::streams表中索引到该流;而id则是流的标识,依赖于具体的容器格式。比如对于MPEG TS格式,id就是pid。
  2. time_base:流的时间基准,是一个实数,该流中媒体数据的pts和dts都将以这个时间基准为粒度。通常,使用av_rescale/av_rescale_q可以实现不同时间基准的转换。
  3. start_time:流的起始时间,以流的时间基准为单位,通常是该流中第一个帧的pts。
  4. duration:流的总时间,以流的时间基准为单位。
  5. need_parsing:对该流parsing过程的控制域。
  6. nb_frames:流内的帧数目。
  7. avg_frame_rate:帧率相关。
  8. codec:指向该流对应的AVCodecContext结构,调用avformat_open_input时生成。
  9. parser:指向该流对应的AVCodecParserContext结构,调用av_find_stream_info时生成。

5、AVIOContext

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

        主要成员:

  1. unsigned char *buffer:缓存开始位置
  2. int buffer_size:缓存大小(默认32768)
  3. unsigned char *buf_ptr:当前指针读取到的位置
  4. unsigned char *buf_end:缓存结束的位置
  5. void *opaque:URLContext结构体

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

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

6、AVPacket

        该结构体是ffmpeg中很重要的一个结构体,它保存了解码后或编码前的数据(仍然是压缩数据)和这些数据的一些附加信息,如显示时间戳(pts)、数据时长、所在媒体的索引等。
        对于视频来说,一个AVPacket通常包含一帧压缩数据,而音频则有可能包含多个压缩的Frame。

        重要的成员变量:

  1. uint8_t *data:压缩编码的数据。
  2. //例如对于H.264来说。1个AVPacket的data通常对应一个NAL。
  3. 注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流.因此在使用FFMPEG进行视音频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。
  4. int size:data的大小
  5. int64_t pts:显示时间戳
  6. int64_t dts:解码时间戳
  7. 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。

        几个常用变量:

  1. uint8_t *data[AV_NUM_DATA_POINTERS]:解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
  2. int linesize[AV_NUM_DATA_POINTERS]:data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽。
  3. int width, height:视频帧宽和高(1920x1080,1280x720…)
  4. int nb_samples:音频的一个AVFrame中可能包含多个音频帧,在此标记包含了几个
  5. int format:解码后原始数据类型(YUV420,YUV422,RGB24…)
  6. int key_frame:是否是关键帧
  7. enum AVPictureType pict_type:帧类型(I,B,P…)
  8. AVRational sample_aspect_ratio:宽高比(16:9,4:3…)
  9. int64_t pts:显示时间戳
  10. int coded_picture_number:编码帧序号
  11. int display_picture_number:显示帧序号
  12. int interlaced_frame:是否是隔行扫描
  13. uint8_t motion_subsample_log2:一个宏块中的运动矢量采样个数,取log

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

1、API调用流程图:

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

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

(1)、注册各大组件

  1. //注册各大组件
  2. av_register_all();

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

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

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

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

(3)、寻找视频流

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

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

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

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

  1. // 寻找解码器 {start
  2. AVCodec *mVcodec = NULL;
  3. AVCodecContext *mAvContext = NULL;
  4. mVcodec = avcodec_find_decoder(origin_par->codec_id);
  5. mAvContext = avcodec_alloc_context3(mVcodec);
  6. if (!mVcodec || !mAvContext) {
  7. return;
  8. }
  9. //不初始化解码器context会导致MP4封装的mpeg4码流解码失败
  10. int ret = avcodec_parameters_to_context(mAvContext, origin_par);
  11. if (ret < 0) {
  12. av_log(NULL, AV_LOG_ERROR, "Error initializing the decoder context.\n");
  13. }
  14. // 打开解码器
  15. if (avcodec_open2(mAvContext, mVcodec, NULL) != 0){
  16. LOGE("打开失败")
  17. return;
  18. }
  19. LOGE("解码器打开成功")
  20. // 寻找解码器 end}

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

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

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

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

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

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

(7)、开始解码

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

  1. // 发送待解码包
  2. int result = avcodec_send_packet(mAvContext, packet);
  3. av_packet_unref(packet);
  4. if (result < 0) {
  5. av_log(NULL, AV_LOG_ERROR, "Error submitting a packet for decoding\n");
  6. continue;
  7. }
  8. // 接收解码数据
  9. while (result >= 0) {
  10. result = avcodec_receive_frame(mAvContext, frame);
  11. if (result == AVERROR_EOF)
  12. break;
  13. else if (result == AVERROR(EAGAIN)) {
  14. result = 0;
  15. break;
  16. } else if (result < 0) {
  17. av_log(NULL, AV_LOG_ERROR, "Error decoding frame\n");
  18. av_frame_unref(frame);
  19. break;
  20. }
  21. av_frame_unref(frame);
  22. }

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

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

  1. while(1)
  2. {
  3. int ret = av_read_frame(avFormatContext, packet);
  4. if (ret != 0){
  5. av_strerror(ret,buf,sizeof(buf));
  6. LOGE("--%s--\n",buf);
  7. av_packet_unref(packet);
  8. break;
  9. }
  10. if (ret >= 0 && packet->stream_index != mVideoStreamIdx) {
  11. av_packet_unref(packet);
  12. continue;
  13. }
  14. {
  15. // 发送待解码包
  16. int result = avcodec_send_packet(mAvContext, packet);
  17. av_packet_unref(packet);
  18. if (result < 0) {
  19. av_log(NULL, AV_LOG_ERROR, "Error submitting a packet for decoding\n");
  20. continue;
  21. }
  22. // 接收解码数据
  23. while (result >= 0){
  24. result = avcodec_receive_frame(mAvContext, frame);
  25. if (result == AVERROR_EOF)
  26. break;
  27. else if (result == AVERROR(EAGAIN)) {
  28. result = 0;
  29. break;
  30. } else if (result < 0) {
  31. av_log(NULL, AV_LOG_ERROR, "Error decoding frame\n");
  32. av_frame_unref(frame);
  33. break;
  34. }
  35. int number_of_written_bytes = av_image_copy_to_buffer(byte_buffer, byte_buffer_size,
  36. (const uint8_t* const *)frame->data, (const int*) frame->linesize,
  37. mAvContext->pix_fmt, mAvContext->width, mAvContext->height, 1);
  38. if (number_of_written_bytes < 0) {
  39. av_log(NULL, AV_LOG_ERROR, "Can't copy image to buffer\n");
  40. av_frame_unref(frame);
  41. continue;
  42. }
  43. // 写文件保存视频数据
  44. fwrite(byte_buffer, number_of_written_bytes, 1, fp_YUV);
  45. fflush(fp_YUV);
  46. av_frame_unref(frame);
  47. }
  48. }
  49. }

(9)、收尾释放资源

        完成过后记得释放资源。

  1. //释放
  2. fclose(fp_YUV);
  3. av_frame_free(&frame);
  4. avcodec_close(mAvContext);
  5. avformat_free_context(avFormatContext);

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

三、demo运行

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

  1. @Override
  2. public void onClick(View view) {
  3. runOnUiThread(new Runnable() {
  4. @Override
  5. public void run() {
  6. String PATH = Environment.getExternalStorageDirectory().getPath();
  7. //视频解码
  8. String input = PATH + File.separator + "input.mp4";
  9. String output = PATH + File.separator + "videoDecodeOut.yuv";
  10. decode_test(input,1,output);
  11. }
  12. });
  13. }

        运行后截图如下:

         界面比较随意,就没有过多设计,点击"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

        

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/446369
推荐阅读
相关标签
  

闽ICP备14008679号