FFmpeg音视频解码原理与实战
好文章,来自【福优学苑@音视频+流媒体】
FFmpeg解码音视频流程简介
音视频编码流程简介(使用 ffmpeg)
第一步:注册组件(编码器、解码器…)
第二步:初始化封装格式上下文
第三步:打开输入文件
第四步:创建输出码流(设置为视频流或音频流)
第五步:查找视频/音频编码器
第六步:打开视频/音频编码器
第七步:写入文件头信息(有些文件头信息)->一般情况下都会有
第八步:循环编码视频像素数据->视频压缩数据
循环编码音频采样数据->音频压缩数据
第九步:将编码后的视频/音频压缩数据写入文件中
第十步:输入像素/采样数据读取完毕后回调函数
作用:输出编码器中剩余AVPacket
音频解码流程
关键函数说明:
avcodec_find_decoder:根据指定的AVCodecID查找注册的解码器。
av_parser_init:初始化AVCodecParserContext。
avcodec_alloc_context3:为AVCodecContext分配内存。
avcodec_open2:打开解码器。
av_parser_parse2:解析获得一个Packet。
avcodec_send_packet:将AVPacket压缩数据给解码器。
avcodec_receive_frame:获取到解码后的AVFrame数据。
av_get_bytes_per_sample: 获取每个sample中的字节数。
关键数据结构说明:
AVCodecParser:用于解析输入的数据流并把它分成一帧一帧的压缩编码数据。
比较形象的说法就是把长长的一段连续的数据“切割”成一段段的数据。
音频解码代码:
#include <stdlib.h> #include <stdio.h> #include <unistd.h> //封装格式 #include "libavformat/avformat.h" //解码 #include "libavcodec/avcodec.h" //缩放 #include "libswscale/swscale.h" #include "libswresample/swresample.h" int main (void) { //1.注册组件 av_register_all(); //封装格式上下文 AVFormatContext *pFormatCtx = avformat_alloc_context(); //2.打开输入音频文件 if (avformat_open_input(&pFormatCtx, "test.mp3", NULL, NULL) != 0) { printf("%s", "打开输入音频文件失败"); return; } //3.获取音频信息 if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { printf("%s", "获取音频信息失败"); return; } //音频解码,需要找到对应的AVStream所在的pFormatCtx->streams的索引位置 int audio_stream_idx = -1; int i = 0; for (; i < pFormatCtx->nb_streams; i++) { //根据类型判断是否是音频流 if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) { audio_stream_idx = i; break; } } //4.获取解码器 //根据索引拿到对应的流,根据流拿到解码器上下文 AVCodecContext *pCodeCtx = pFormatCtx->streams[audio_stream_idx]->codec; //再根据上下文拿到编解码id,通过该id拿到解码器 AVCodec *pCodec = avcodec_find_decoder(pCodeCtx->codec_id); if (pCodec == NULL) { printf("%s", "无法解码"); return; } //5.打开解码器 if (avcodec_open2(pCodeCtx, pCodec, NULL) < 0) { printf("%s", "编码器无法打开"); return; } //编码数据 AVPacket *packet = av_malloc(sizeof(AVPacket)); //解压缩数据 AVFrame *frame = av_frame_alloc(); //frame->16bit 44100 PCM 统一音频采样格式与采样率 SwrContext *swrCtx = swr_alloc(); //重采样设置选项-----------------------------------------------------------start //输入的采样格式 enum AVSampleFormat in_sample_fmt = pCodeCtx->sample_fmt; //输出的采样格式 16bit PCM enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16; //输入的采样率 int in_sample_rate = pCodeCtx->sample_rate; //输出的采样率 int out_sample_rate = 44100; //输入的声道布局 uint64_t in_ch_layout = pCodeCtx->channel_layout; //输出的声道布局 uint64_t out_ch_layout = AV_CH_LAYOUT_MONO; swr_alloc_set_opts(swrCtx, out_ch_layout, out_sample_fmt, out_sample_rate, in_ch_layout, in_sample_fmt, in_sample_rate, 0, NULL); swr_init(swrCtx); //重采样设置选项-----------------------------------------------------------end //获取输出的声道个数 int out_channel_nb = av_get_channel_layout_nb_channels(out_ch_layout); //存储pcm数据 uint8_t *out_buffer = (uint8_t *) av_malloc(2 * 44100); FILE *fp_pcm = fopen("out.pcm", "wb"); int ret, got_frame, framecount = 0; //6.一帧一帧读取压缩的音频数据AVPacket while (av_read_frame(pFormatCtx, packet) >= 0) { if (packet->stream_index == audio_stream_idx) { //解码AVPacket->AVFrame ret = avcodec_decode_audio4(pCodeCtx, frame, &got_frame, packet); if (ret < 0) { printf("%s", "解码完成"); } //非0,正在解码 if (got_frame) { printf("解码%d帧", framecount++); swr_convert(swrCtx, &out_buffer, 2 * 44100, frame->data, frame->nb_samples); //获取sample的size int out_buffer_size = av_samples_get_buffer_size(NULL, out_channel_nb, frame->nb_samples, out_sample_fmt, 1); //写入文件进行测试 fwrite(out_buffer, 1, out_buffer_size, fp_pcm); } } av_free_packet(packet); } fclose(fp_pcm); av_frame_free(&frame); av_free(out_buffer); swr_free(&swrCtx); avcodec_close(pCodeCtx); avformat_close_input(&pFormatCtx); return 0; }
视频解码
在视频解码前,先了解以下几个基本的概念:
编解码器(CODEC):能够进行视频和音频压缩(CO)与解压缩(DEC),是视频编解码的核心部分。
容器/多媒体文件(Container/File):没有了解视频的编解码之前,总是错误的认为平常下载的电影的文件的后缀(avi,mkv,rmvb等)就是视频的编码方式。事实上,刚才提到的几种文件的后缀
并不是视频的编码方式,只是其封装的方式。一个视频文件通常有视频数据、音频数据以及字幕等,封装的格式决定这些数据在文件中是如何的存放的,封装在一起音频、视频等数据组成的多媒体文件,也可以叫做容器(其中包含了视音频数据)。所以,只看多媒体文件的后缀名是难以知道视音频的编码方式的。
流数据 Stream,例如视频流(Video Stream),音频流(Audio Stream)。流中的数据元素被称为帧Frame。
FFmpeg视频解码过程
通常来说,FFmpeg的视频解码过程有以下几个步骤:
注册所支持的所有的文件(容器)格式及其对应的CODEC av_register_all()
打开文件 avformat_open_input()
从文件中提取流信息 avformat_find_stream_info()
在多个数据流中找到视频流 video stream(类型为MEDIA_TYPE_VIDEO)
查找video stream 相对应的解码器 avcodec_find_decoder
打开解码器 avcodec_open2()
为解码帧分配内存 av_frame_alloc()
从流中读取读取数据到Packet中 av_read_frame()
对video 帧进行解码,调用 avcodec_decode_video2()
解码过程的具体说明
1. 注册
av_register_all该函数注册支持的所有的文件格式(容器)及其对应的CODEC,只需要调用一次,故一般放在main函数中。也可以注册某个特定的容器格式,但通常来说不需要这么做。
2. 打开文件
avformat_open_input该函数读取文件的头信息,并将其信息保存到AVFormatContext结构体中。
3. 获取必要的CODEC参数
avformat_open_input通过解析多媒体文件或流的头信息及其他的辅助数据,能够获取到足够多的关于文件、流和CODEC的信息,并将这些信息填充到AVFormatContext结构体中。但任何一种多媒体格式(容器)提供的信息都是有限的,而且不同的多媒体制作软件对头信息的设置也不尽相同,在制作多媒体文件的时候难免会引入一些错误。也就是说,仅仅通过avformat_open_input并不能保证能够获取所需要的信息,所以一般要使用
avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)
avformat_find_stream_info主要用来获取必要的CODEC参数,设置到ic->streams[i]->codec。
4. 打开解码器
经过上面的步骤,已经将文件格式信息读取到了AVFormatContext中,要打开流数据相应的CODEC需要经过下面几个步骤
找到视频流 video stream
一个多媒体文件包含有多个原始流,例如 movie.mkv这个多媒体文件可能包含下面的流数据
原始流 1 h.264 video
原始流 2 aac audio for Chinese
原始流 3 aac audio for English
原始流 4 Chinese Subtitle
原始流 5 English Subtitle
5. 读取数据帧并解码
已经有了相应的解码器,下面的工作就是将数据从流中读出,并解码为没有压缩的原始数据
AVPacket packet;
while (av_read_frame(pFormatCtx, &packet) >= 0)
{
if (packet.stream_index == videoStream)
{
int frameFinished = 0;
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
if (frameFinished)
{
doSomething();
}
}
}
上述代码调用av_read_frame将数据从流中读取数据到packet中,并调用avcodec_decode_video2对读取的数据进行解码。
6. 关闭
需要关闭avformat_open_input打开的输入流,avcodec_open2打开的CODEC
avcodec_close(pCodecCtxOrg);
avformat_close_input(&pFormatCtx);
视频解码代码
#include <stdio.h> #include <stdlib.h> //编码 #include "libavcodec/avcodec.h" //封装格式处理 #include "libavformat/avformat.h" //像素处理 #include "libswscale/swscale.h" int main() { //获取输入输出文件名 const char *input = "test.mp4"; const char *output = "test.yuv"; //1.注册所有组件 av_register_all(); //封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息 AVFormatContext *pFormatCtx = avformat_alloc_context(); //2.打开输入视频文件 if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) { printf("%s","无法打开输入视频文件"); return; } //3.获取视频文件信息 if (avformat_find_stream_info(pFormatCtx,NULL) < 0) { printf("%s","无法获取视频文件信息"); return; } //获取视频流的索引位置 //遍历所有类型的流(音频流、视频流、字幕流),找到视频流 int v_stream_idx = -1; int i = 0; //number of streams for (; i < pFormatCtx->nb_streams; i++) { //流的类型 if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { v_stream_idx = i; break; } } if (v_stream_idx == -1) { printf("%s","找不到视频流\n"); return; } //只有知道视频的编码方式,才能够根据编码方式去找到解码器 //获取视频流中的编解码上下文 AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec; //4.根据编解码上下文中的编码id查找对应的解码 AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id); if (pCodec == NULL) { printf("%s","找不到解码器\n"); return; } //5.打开解码器 if (avcodec_open2(pCodecCtx,pCodec,NULL)<0) { printf("%s","解码器无法打开\n"); return; } //输出视频信息 printf("视频的文件格式:%s",pFormatCtx->iformat->name); printf("视频时长:%d", (pFormatCtx->duration)/1000000); printf("视频的宽高:%d,%d",pCodecCtx->width,pCodecCtx->height); printf("解码器的名称:%s",pCodec->name); //准备读取 //AVPacket用于存储一帧一帧的压缩数据(H264) //缓冲区,开辟空间 AVPacket *packet = (AVPacket*)av_malloc(sizeof(AVPacket)); //AVFrame用于存储解码后的像素数据(YUV) //内存分配 AVFrame *pFrame = av_frame_alloc(); //YUV420 AVFrame *pFrameYUV = av_frame_alloc(); //只有指定了AVFrame的像素格式、画面大小才能真正分配内存 //缓冲区分配内存 uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height)); //初始化缓冲区 avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height); //用于转码(缩放)的参数,转之前的宽高,转之后的宽高,格式等 struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width,pCodecCtx->height,pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); int got_picture, ret; FILE *fp_yuv = fopen(output, "wb+"); int frame_count = 0; //6.一帧一帧的读取压缩数据 while (av_read_frame(pFormatCtx, packet) >= 0) { //只要视频压缩数据(根据流的索引位置判断) if (packet->stream_index == v_stream_idx) { //7.解码一帧视频压缩数据,得到视频像素数据 ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet); if (ret < 0) { printf("%s","解码错误"); return; } //为0说明解码完成,非0正在解码 if (got_picture) { //AVFrame转为像素格式YUV420,宽高 //2 6输入、输出数据 //3 7输入、输出画面一行的数据的大小 AVFrame 转换是一行一行转换的 //4 输入数据第一列要转码的位置 从0开始 //5 输入画面的高度 sws_scale(sws_ctx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); //输出到YUV文件 //AVFrame像素帧写入文件 //data解码后的图像像素数据(音频采样数据) //Y 亮度 UV 色度(压缩了) 人对亮度更加敏感 //U V 个数是Y的1/4 int y_size = pCodecCtx->width * pCodecCtx->height; fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv); fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv); fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv); frame_count++; printf("解码第%d帧\n",frame_count); } } //释放资源 av_free_packet(packet); } fclose(fp_yuv); av_frame_free(&pFrame); avcodec_close(pCodecCtx); avformat_free_context(pFormatCtx); }
音视频解码完整代码:
#include <stdio.h> #ifdef __cplusplus extern "C" { #endif #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #ifdef __cplusplus }; #endif int openCodecContext(const AVFormatContext *pFormatCtx, int *pStreamIndex, enum AVMediaType type, AVCodecContext **ppCodecCtx) { int streamIdx = -1; // 获取流下标 for (int i = 0; i < pFormatCtx->nb_streams; i++) { if (pFormatCtx->streams[i]->codec->codec_type == type) { streamIdx = i; break; } } if (streamIdx == -1) { printf("find video stream failed!\n"); exit(-2); } // 寻找解码器 AVCodecContext *pCodecCtx = pFormatCtx->streams[streamIdx]->codec; AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id); if (NULL == pCodec) { printf("avcode find decoder failed!\n"); exit(-2); } //打开解码器 if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) { printf("avcode open failed!\n"); exit(-2); } *ppCodecCtx = pCodecCtx; *pStreamIndex = streamIdx; return 0; } int main(void) { AVFormatContext *pInFormatCtx = NULL; AVCodecContext *pVideoCodecCtx = NULL; AVCodecContext *pAudioCodecCtx = NULL; AVPacket *pPacket = NULL; AVFrame *pFrame = NULL; int ret; /* 支持本地文件和网络url */ const char streamUrl[] = "./test.flv"; /* 1. 注册 */ av_register_all(); pInFormatCtx = avformat_alloc_context(); /* 2. 打开流 */ if(avformat_open_input(&pInFormatCtx, streamUrl, NULL, NULL) != 0) { printf("Couldn't open input stream.\n"); return -1; } /* 3. 获取流的信息 */ if(avformat_find_stream_info(pInFormatCtx, NULL) < 0) { printf("Couldn't find stream information.\n"); return -1; } int videoStreamIdx = -1; int audioStreamIdx = -1; /* 4. 寻找并打开解码器 */ openCodecContext(pInFormatCtx, &videoStreamIdx, AVMEDIA_TYPE_VIDEO, &pVideoCodecCtx); openCodecContext(pInFormatCtx, &audioStreamIdx, AVMEDIA_TYPE_AUDIO, &pAudioCodecCtx); pPacket = av_packet_alloc(); pFrame = av_frame_alloc(); int cnt = 30; while (cnt--) { /* 5. 读流数据, 未解码的数据存放于pPacket */ ret = av_read_frame(pInFormatCtx, pPacket); if (ret < 0) { printf("av_read_frame error\n"); break; } /* 6. 解码, 解码后的数据存放于pFrame */ /* 视频解码 */ if (pPacket->stream_index == videoStreamIdx) { avcodec_decode_video2(pVideoCodecCtx, pFrame, &ret, pPacket); if (ret == 0) { printf("video decodec error!\n"); continue; } printf("* * * * * * video * * * * * * * * *\n"); printf("___height: [%d]\n", pFrame->height); printf("____width: [%d]\n", pFrame->width); printf("pict_type: [%d]\n", pFrame->pict_type); printf("___format: [%d]\n", pFrame->format); printf("* * * * * * * * * * * * * * * * * * *\n\n"); } /* 音频解码 */ if (pPacket->stream_index == audioStreamIdx) { avcodec_decode_audio4(pAudioCodecCtx, pFrame, &ret, pPacket); if (ret < 0) { printf("audio decodec error!\n"); continue; } printf("* * * * * * audio * * * * * * * * * *\n"); printf("____nb_samples: [%d]\n", pFrame->nb_samples); printf("__samples_rate: [%d]\n", pFrame->sample_rate); printf("channel_layout: [%lu]\n", pFrame->channel_layout); printf("________format: [%d]\n", pFrame->format); printf("* * * * * * * * * * * * * * * * * * *\n\n"); } av_packet_unref(pPacket); } av_frame_free(&pFrame); av_packet_free(&pPacket); avcodec_close(pVideoCodecCtx); avcodec_close(pAudioCodecCtx); avformat_close_input(&pInFormatCtx); return 0; }
好文章,来自【福优学苑@音视频+流媒体】
***【在线视频教程】***