FFmpeg编程--解码流程

一、解码流程总览

在这里插入图片描述


二、解码流程分解

第一步:注册

使用FFmpeg对应的库,都需要进行注册,注册了这个才能正常使用编码器和解码器;

///第一步
av_register_all();

第二步:打开文件

打开文件,根据文件名信息获取对应的FFmpeg全局上下文

///第二步
AVFormatContext *pFormatCtx;	//文件上下文,描述了一个媒体文件或媒体流的构成和基本信息

pFormatCtx = avformat_alloc_context();	//分配指针

if (avformat_open_input(&pFormatCtx, file_path, NULL, NULL) != 0) {	//打开文件,信息存储到文件上下文中,后续对针对文件上下文即可
	printf("无法打开文件");
    return -1;
}

第三步:探测流信息

一定要探测流信息,拿到流编码的编码格式,不探测流信息则器流编码器拿到的编码类型可能为空,后续进行数据转换的时候就无法知晓原始格式,导致错误;

///第三步
//探寻文件中是否存在信息流
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
	printf("文件中没有发现信息流");
	return -1;
}

//探寻文件中是否存储视频流
int videoStream = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++) {
	if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
		videoStream = i;
	}
}
//如果videoStream为-1 说明没有找到视频流
if (videoStream == -1) {
	printf("文件中未发现视频流");
	return -1;
}

//探寻文件中是否存在音频流
int audioStream = -1
for (i = 0; i < pFormatCtx->nb_streams; i++) {
	if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
		audioStream = i;
	}
}
//如果audioStream 为-1 说明没有找到音频流
if (audioStream == -1) {
	printf("文件中未发现音频流");
	return -1;
}

第四步:查找对应的解码器

依据流的格式查找解码器,软解码还是硬解码是在此处决定的,但是特别注意是否支持硬件,需要自己查找本地的硬件解码器对应的标识,并查询其是否支持。普遍操作是,枚举支持文件后缀解码的所有解码器进行查找,查找到了就是可以硬解了;

注意:解码时查找解码器,编码时查找编码器,两者函数不同,不要弄错了,否则后续能打开但是数据是错的;

///第四步
AVCodecContext *pCodecCtx;      //描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息
AVCodec *pCodec;                //存储编解码器信息的结构体

//查找解码器
pCodecCtx = pFormatCtx->streams[videoStream]->codec;    //获取视频流中编码器上下文
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);     //获取视频流的编码器信息

if (pCodec == NULL) {
    printf("未发现编码器");
    return -1;
}

第五步:打开解码器

打开获取到的解码器

///第五步
//打开解码器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
    printf("无法打开编码器");
    return -1;
}

第六步:申请缩放数据格式转换结构体

基本上解码的数据都是yuv系列格式,但是我们显示的数据是rgb等相关颜色空间的数据,所以此处转换结构体就是进行转换前导转换后的描述,给后续转换函数提供转码依据,是很关键并且非常常用的结构体;

///第六步
static struct SwsContext *img_convert_ctx;  //用于视频图像的转换

img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
            pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height,
            AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);

第七步:计算缩放颜色空间转换后缓存大小

///第七步
int numBytes;	//字节数
numBytes = avpicture_get_size(AV_PIX_FMT_BGR24, pCodecCtx->width,pCodecCtx->height);

第八步:申请缓存区,将AVFrama的data映射到单独的outBuffer上

申请一个缓存区outBuffer,fill到我们目标帧数据的data上,比如rgb数据,QAVFrame的data上存的是有指定格式的数据且存储有规则,而fill到outBuffer(自己申请的目标格式一帧缓存区),则是我们需要的数据格式存储顺序;

例如:解码转换后的数据为rgb888,实际直接使用data数据是错误的,但是用outBuffer就是对的,所以此处应该是FFmpeg的fill函数做了一些转换;

///第七步
AVFrame *pFrame, *pFrameRGB;    //存储音视频原始数据(即未被编码的数据)的结构体
pFrame = av_frame_alloc();
pFrameRGB = av_frame_alloc();

uint8_t *out_buffer;            //缓存
out_buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));

avpicture_fill((AVPicture *) pFrameRGB, out_buffer, AV_PIX_FMT_BGR24,
            pCodecCtx->width, pCodecCtx->height);

第九步:循环解码

1、获取一帧packet

int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据

int ret, got_picture;
while(1) {
	if (av_read_frame(pFormatCtx, packet) < 0) {	//读取一帧packet数据包
    	break; //这里认为视频读取完了
   	}

	......
}

2、解码获取原始数据

int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据

int ret, got_picture;
while(1) {
	if (av_read_frame(pFormatCtx, packet) < 0) {	//读取一帧packet数据包
    	break; //这里认为视频读取完了
   	}

	if (packet->stream_index == videoStream) {
    	ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet);	//解码packet包,原始数据存入pFrame中

        if (ret < 0) {
       		printf("decode error.");
            return -1;
        }

       	......
    }
}

3、数据转换

int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据

int ret, got_picture;
while(1) {
	if (av_read_frame(pFormatCtx, packet) < 0) {	//读取一帧packet数据包
    	break; //这里认为视频读取完了
   	}

	if (packet->stream_index == videoStream) {
    	ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet);	//解码packet包,原始数据存入pFrame中

        if (ret < 0) {	//是否解析成功?
       		printf("decode error.");
            return -1;
        }

        if (got_picture) {	//是否get一帧?
        	//数据转换	
        	sws_scale(img_convert_ctx,	
        			(uint8_t const * const *) pFrame->data,
                   	 pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
                     pFrameRGB->linesize);

            ......
      	}
      	......
    }
}

4、自由操作

int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据

int ret, got_picture;
while(1) {
	if (av_read_frame(pFormatCtx, packet) < 0) {	//读取一帧packet数据包
    	break; //这里认为视频读取完了
   	}

	if (packet->stream_index == videoStream) {
    	ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet);	//解码packet包,原始数据存入pFrame中

        if (ret < 0) {	//是否解析成功?
       		printf("decode error.");
            return -1;
        }

        if (got_picture) {	//是否get一帧?
        	//数据转换	
        	sws_scale(img_convert_ctx,	
        			(uint8_t const * const *) pFrame->data,
                   	 pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
                     pFrameRGB->linesize);

			//自由操作,SaveFrame是自定义函数
            SaveFrame(pFrameRGB, pCodecCtx->width,pCodecCtx->height,index++); //保存图片
            if (index > 50) return 0; //这里我们就保存50张图片
      	}
     	
     	//释放QAVPacket
        av_free_packet(packet);
    }
}

5、释放QAVPacket

在进入循环解码前进行了av_new_packet,循环中未av_free_packet,造成内存溢出;
在进入循环解码前进行了av_new_packet,循环中进行av_free_pakcet,那么一次new对应无数次free,在编码器上是不符合前后一一对应规范的。
查看源代码,其实可以发现av_read_frame时,自动进行了av_new_packet(),那么其实对于packet,只需要进行一次av_packet_alloc()即可,解码完后av_free_packet。

//释放QAVPacket
 av_free_packet(packet);

第十步:释放资源

全部解码完成后,按照申请顺序,进行对应资源的释放。

av_free(out_buffer);
av_free(pFrameRGB);

sws_freeContext(img_convert_ctx);

avcodec_close(pCodecCtx);	//关闭编码/解码器

avformat_close_input(&pFormatCtx);	//关闭文件全局上下文

三、完整代码

#include "mainwindow.h"

#include <QApplication>

//===============================================================================
extern "C"{
    #include "libavcodec/avcodec.h"
    #include "libavformat/avformat.h"
    #include "libavutil/pixfmt.h"
    #include "libswscale/swscale.h"
}

#include <stdio.h>

///现在我们需要做的是让SaveFrame函数能把RGB信息定稿到一个PPM格式的文件中。
///我们将生成一个简单的PPM格式文件,请相信,它是可以工作的。
void SaveFrame(AVFrame *pFrame, int width, int height,int index)
{
  FILE *pFile;
  char szFilename[32];
  int  y;

  // Open file
  sprintf(szFilename, "frame%d.ppm", index);
  pFile=fopen(szFilename, "wb");

  if(pFile==NULL)
    return;

  // Write header
  fprintf(pFile, "P6%d %d255", width, height);

  // Write pixel data
  for(y=0; y<height; y++)
  {
    fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
  }

  // Close file
  fclose(pFile);
}

int main(int argc, char *argv[])
{
    //变量定义
    //==================================================================================
    char *file_path = "C:/Users/wangjichuan/Desktop/2.mp4";   //文件路径


    ///=====第一步=====
    //初始化FFMPEG  调用了这个才能正常使用编码器和解码器
    av_register_all();
    ///===================================================================


    ///=====第二步=====
    AVFormatContext *pFormatCtx;    //描述了一个媒体文件或媒体流的构成和基本信息
    pFormatCtx = avformat_alloc_context();  //分配一个解封装上下文指针

    //打开文件
    if (avformat_open_input(&pFormatCtx, file_path, NULL, NULL) != 0) { //文件信息存储到文件上下文中,后续对它进行处理即可
        printf("无法打开文件");
        return -1;
    }
    ///===================================================================

    ///=====第三步=====
    //探寻文件中是否存在信息流
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        printf("文件中没有发现信息流");
        return -1;
    }

    //循环查找视频中包含的流信息,直到找到视频类型的流
    //便将其记录下来 保存到videoStream变量中
    //这里我们现在只处理视频流  音频流先不管他
    int videoStream = -1;
    int i;
    for (i = 0; i < pFormatCtx->nb_streams; i++) {
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStream = i;
        }
    }

    //如果videoStream为-1 说明没有找到视频流
    if (videoStream == -1) {
        printf("文件中未发现视频流");
        return -1;
    }
    ///===================================================================

    ///=====第四步=====
    AVCodecContext *pCodecCtx;      //描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息
    AVCodec *pCodec;                //存储编解码器信息的结构体

    //查找解码器
    pCodecCtx = pFormatCtx->streams[videoStream]->codec;    //获取视频流中编码器上下文
    pCodec = avcodec_find_decoder(pCodecCtx->codec_id);     //获取视频流的编码器

    if (pCodec == NULL) {
        printf("未发现编码器");
        return -1;
    }
    ///===================================================================

    ///=====第五步=====
    //打开解码器
    //==================================================================================
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        printf("无法打开编码器");
        return -1;
    }
    ///===================================================================

    ///=====第六步=====
    static struct SwsContext *img_convert_ctx;  //用于视频图像的转换
    img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
            pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height,
            AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
    ///===================================================================

    ///=====第七步=====
    int numBytes;
    numBytes = avpicture_get_size(AV_PIX_FMT_BGR24, pCodecCtx->width,pCodecCtx->height);
    ///===================================================================

    ///=====第八步=====
    AVFrame *pFrame, *pFrameRGB;    //存储音视频原始数据(即未被编码的数据)的结构体
    pFrame = av_frame_alloc();
    pFrameRGB = av_frame_alloc();

    uint8_t *out_buffer;            //缓存
    out_buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
    avpicture_fill((AVPicture *) pFrameRGB, out_buffer, AV_PIX_FMT_BGR24,
            pCodecCtx->width, pCodecCtx->height);
    ///===================================================================

    ///=====第九步=====
    AVPacket *packet;               //保存了解复用(demuxer)之后,解码(decode)之前的数据(仍然是压缩后的数据)和关于这些数据的一些附加的信息
    int ret, got_picture;

    int y_size = pCodecCtx->width * pCodecCtx->height;
    packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
    av_new_packet(packet, y_size); //分配packet的数据

    av_dump_format(pFormatCtx, 0, file_path, 0); //输出视频信息

    int index = 0;

    while (1)
    {
        if (av_read_frame(pFormatCtx, packet) < 0) {
            break; //这里认为视频读取完了
        }

        if (packet->stream_index == videoStream) {
            ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet);

            if (ret < 0) {
                printf("decode error.");
                return -1;
            }

            if (got_picture) {
                sws_scale(img_convert_ctx,
                        (uint8_t const * const *) pFrame->data,
                        pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
                        pFrameRGB->linesize);

                SaveFrame(pFrameRGB, pCodecCtx->width,pCodecCtx->height,index++); //保存图片
                if (index > 50) return 0; //这里我们就保存50张图片
            }
        }
        av_free_packet(packet);
    }
    ///===================================================================

    ///=====第十步=====
    av_free(out_buffer);
    av_free(pFrameRGB);
    sws_freeContext(img_convert_ctx);
    avcodec_close(pCodecCtx);
    avformat_close_input(&pFormatCtx);
    ///===================================================================

    return 0;
}


四、FFmpeg解码相关变量

1、AVFormatContext

AVFormatContext描述了一个媒体文件或媒体流的构成和基本信息,位于avformat.h文件中;

2、AVInputFormat

AVInputFormat是类似COM接口的数据结构,表示输入文件容器格式,着重于功能函数,一种文件容器格式对应一个AVInputFormat结构,在程序运行时有多个实例,位于avoformat.h文件中;

3、AVDictionary

AVDictionary是一个字典集合,键值对,用于配置相关信息;

4、AVCodecContext

AVCodecContext是一个描述编码器上下文的数据结构,包含了众多编码器需要的参数信息,位于avcodec.h文件中;

5、AVPacket

AVPacket是FFmpeg中很重要的一个数据结构,它保存了解复用(demuxer)之后,解码(decode)之前的数据(仍然是压缩后的数据)和关于这些数据的一些附加的信息,如显示时间戳(pts),解码时间戳(dts),数据时长等;
使用前,使用av_packet_alloc()分配;

6、AVCodec

AVCodec是存储编码器信息的结构体,位于avcodec.h

7、AVFrame

AVFrame中存储的是经过解码后的原始数据。在解码中,AVFrame是解码器的输出;在编码中,AVFrame是编码器的输入;
使用前,使用av_frame_alloc()进行分配;

8、struct SwsContext

使用前,使用sws_getContext()进行获取,主要用于视频图像的转换;


五、FFmpeg解码流程相关函数原型

1、av_register_all

初始化libavformat并注册所有muxer、demuxer和协议;如果不调用此函数,则可以选择想要指定注册支持的哪种格式,通过av_register_input_format()、av_register_output_format();

void av_register_all(void)

2、avformat_open_input

打开输入流并读取标头;此时编解码器还未打开;流必须使用avformat_close_input()关闭,返回0成功,小于0失败错误码;

int avformat_open_input(AVFormatContext **ps,
						const char *url,
						AVInputFormat *fmt,
						AVDictionary **options);
  • ps:指向用户提供的AVFormatContext(由avformat_alloc_context分配)的指针;
  • url:要打开的流的url;
  • fmt:fmt如果非空,则此参数强制使用特定的输入格式,否则将自动检测格式;
  • options:包含AVFormatContext和demuxer私有选项的字典。返回时,此参数将销毁并替换为包含找不到的选项;都有效则返回为空;

3、avformat_find_stream_info

读取检测媒体文件的数据包以获取具体的流信息,如媒体存入的编码格式;

int avformat_find_stream_info(AVFormatContext *ic,AVDictionary **options);
  • ic:媒体文件上下文;
  • options:字典,一些配置选项;

4、avcodec_find_decoder

查找具有匹配编解码器ID的已注册解码器,解码时,已经获取到了,注册的解码器可以通过枚举查看;

AVCodec *avcodec_find_decoder(enum AVCodecID id);

5、avcodec_open2

初始化AVCodecContext以使用给定的AVCodec;

int avcodec_open2(AVCodecContext *avctx,
				  const AVCodec *codec,
				  AVDictionary **options);

6、sws_getContext

分配并返回一个SwsContext。需要它来执行sws_scale()进行缩放/旋转操作;

struct SwsContext *sws_getContext(int srcW,
								  int srcH,
								  enum AVPixelFormat srcFormat,
								  int dstW,
								  int dstH,
								  enum AVPixelFormat dstFormat,
								  int flags,
								  SwsFilter *srcFilter,
								  SwsFilter *dstFilter,
								  const double *param);

7、avpictrue_get_size

返回存储具有给定参数的图像的缓存区域大小;

int avpicture_get_size(enum AVPixelFormat pix_fmt, int widget, int height);
  • pix_fmt:图像的像素格式;
  • width:图像的像素宽度;
  • height:图像的像素高度;

8、avpictrue_fill

根据指定的图像、提供的数组设置数据指针和线条大小参数;

int avpicture_fill(AVPicture *picture,
				   const uint8_t *ptr,
				   enum AVPixelFormat pix_fmt,
				   int width,
				   int height);
  • picture:输入AVFrame指针,强制转换为AVPicture即可;
  • ptr:映射到的缓存区,开发者自己申请的存放图像数据的缓存区;
  • pix_fmt:图像数据的编码格式;
  • width:图像像素宽度;
  • height:图像像素高度;

9、av_read_frame

返回流的下一帧;
此函数返回存储在文件中的内容,不对有效的帧进行验证;获取存储在文件中的帧中,并未每个调用返回一个;不会省略有效帧之间的无效数据,以便给解码器最大可用于解码的信息;
返回0是成功,小于0则是错误,大于0则是文件末尾,所以大于等于0是返回成功;

10、avcodec_decode_video2

将大小为avpkt->size from avpkt->data的视频帧解码为图片。
一些解码器可以支持单个avpkg包中的多个帧,解码器将只解码第一帧;出错时返回负值,否则返回字节数,如果没有帧可以解压缩,则为0;

int avcodec_decode_video2(AVCodecContext *avctx,
						  AVFrame *picture,
						  int *got_picture_ptr,
						  const AVPacket *avpkt);
  • avctx:编解码器上下文;
  • picture:将解码视频帧存储在AVFrame中;
  • got_picture_ptr:输入缓冲区的AVPacket;
  • avpkt:如果没有帧可以解压,那么得到的图片是0;否则,它是非零的;

11、sws_scale

在srcSlice中缩放图像切片,并将结果缩放在dst中切片图像。切片是连续的序列图像中的行。

int sws_scale(struct SwsContext *c,
			  const uint8_t *const srcSlice[],
			  const int srcStride[],
			  int srcSliceY,
			  int srcSliceH,
			  uint8_t *const dst[],
			  const int dstStride[]);
  • c:以前用创建的缩放上下文sws+getContext();
  • srcSlice[]:包含指向源片段,就是AVFrame的data;
  • srcStride[]:包含每个平面的跨步的数组,其实就是AVFrame的linesize;
  • srcSliceY:切片在源图像中的位置,从开始计数0对应切片第一行的图像,所以直接填0即可;
  • srcSliceH:源切片的像素高度;
  • dst[]:目标数据地址映像,是目标AVFrame的data;
  • dstStride[]:目标每个平面的跨步的数组,就是linesize;

十二、av_free_packet

释放一个包;

void av_free_packet(AVPacket *pkt);

十三、avcodec_close

关闭给定的avcodeContext并释放与之关联的所有数据(但不是AVCodecContext本身);

int avcodec_close(AVCodecContext *avctx);

十四、avformat_close_input

关闭打开的输入AVFormatContext,释放它和它的所有内容,并将*s设置为空;

void avformat_close_input(AVFormatContext **s);
  • 9
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贝勒里恩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值