FFmpeg视频解码器
视频解码知识
纯洁的视频解码流程
- 压缩编码数据->像素数据。
- 例如解码H.264,就是“H.264码流->YUV”
一般的视频解码流程
VC下FFmpeg开发环境搭建
新建工程
- 建Win32控制台程序即可
拷贝FFmpeg开发文件
- 头文件(*.h)拷贝至项目文件夹的include子文件夹下
- 导入库文件(*.lib)拷贝至项目文件夹的lib子文件夹下
- 动态库文件(*.dll)拷贝至项目文件夹下
配置开发文件
头文件配置:
- 配置属性->C/C++->常规->附加包含目录,输入“include”(刚才拷贝头文件的目录)
导入库配置:
- 配置属性->链接器->常规->附加库目录,输入“lib” (刚才拷贝库文件的目录)
- 配置属性->链接器->输入->附加依赖项,输入“avcodec.lib; avformat.lib; avutil.lib; avdevice.lib; avfilter.lib; postproc.lib; swresample.lib; swscale.lib”(导入库的文件名)
动态库不用配置
测试
包含头文件
- 如果是C语言中使用FFmpeg,则直接使用下面代码
#include "libavcodec/avcodec.h"
- 如果是C++语言中使用FFmpeg,则使用下面代码
#define __STDC_CONSTANT_MACROS
extern "C"
{
#include "libavcodec/avcodec.h "
}
测试程序
#define __STDC_CONSTANT_MACROS
#include "stdafx.h"
extern "C"
{
#include "libavcodec/avcodec.h "
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("%s\n", avcodec_configuration());//FFmpeg的配置信息
getchar();
return 0;
}
FFmepg库简介(用FFmpeg开发时会经常用到)
- avcodec:编解码(最重要的库)。
- avformat:封装格式处理。
- avfilter:滤镜特效处理。
- avdevice:各种设备的输入输出。
- avutil:工具库(大部分库都需要这个库的支持)。
- postproc:后加工。
- swresample:音频采样数据格式转换。
- swscale:视频像素数据格式转换。
FFmpeg解码的函数
解码流程
- av_register_all(): 所有程序都用这个函数开头初始化,注册所有的组件
- avformat_open_input(): 打开输入视频文件
- avformat_find_stream_info(): 获取输入视频文件的信息,比如分辨率、编码方式等等
- avcodec_find_decoder(): 根据编码方式找到编解码器(codec)
- avcodec_open2():打开codec
- av_read_frame():每调用一次,读一帧
- AVPacket:结构体,装的是H.264
- avcodec_decode_video2(): 解码一帧压缩数据
- AVFrame: 结构体,装的是YUV
其它常用解码函数
- avcodec_close():关闭解码器。
- avformat_close_input():关闭输入视频文件。
FFmpeg解码的数据结构
数据结构的层次关系
数据结构简介
- AVFormatContext
封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息,最外层的信息。 - AVInputFormat
每种封装格式(例如FLV, MKV, MP4, AVI)对应一个该结构体。 - AVStream
视频文件中每个视频(音频)流对应一个该结构体。变长数组,但一般的视频文件里就两个,分别是视频流(下标0)和音频流(下表1) - AVCodecContext
编码器上下文结构体,保存了视频(音频)编解码相关信息。 - AVCodec
每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体。 - AVPacket
存储一帧压缩编码数据。 - AVFrame
存储一帧解码后像素(采样)数据。
FFmpeg数据结构分析
这里只写一些常用的属性,详细的请见源码中给出的定义
AVFormatContext
- iformat:输入视频的AVInputFormat
- nb_streams :输入视频的AVStream 个数
- streams :输入视频的AVStream []数组
- duration :输入视频的时长(以微秒为单位)
- bit_rate :输入视频的码率
AVInputFormat
- name:封装格式名称
- long_name:封装格式的长名称
- extensions:封装格式的扩展名
- id:封装格式ID
- 一些封装格式处理的接口函数
AVStream
- id:序号
- codec:该流对应的AVCodecContext
- time_base:该流的时基,是一个分数(结构中的存的是分子和分母这两个整数)。由于每一帧出现的时间需要被记录,而这个数是非整数的,不好记录,因此用时基作为单位,这样存每一帧的时间就可以用整数了,具体用的时候和time_base这个分数一乘即可。
- r_frame_rate:该流的帧率,一秒有多少帧
AVCodecContext
- codec:编解码器的AVCodec
- width, height:图像的宽高(只针对视频)
- pix_fmt:像素格式(只针对视频)
- sample_rate:采样率(只针对音频)
- channels:声道数(只针对音频)
- sample_fmt:采样格式(只针对音频)
AVCodec
- name:编解码器名称
- long_name:编解码器长名称
- type:编解码器类型
- id:编解码器ID
- 一些编解码的接口函数
AVPacket
装H.264
- pts:显示时间戳,这是显示的时间,需要乘上时基
- dts :解码时间戳,这是解码的时间,顺序和显示的顺序不一定一样
- data :压缩编码数据
- size :压缩编码数据大小
- stream_index :所属的AVStream,保存索引
AVFrame
装YUV
- data:解码后的图像像素数据(音频采样数据)。
- linesize:对视频来说是图像中一行像素的大小;对音频来说是整个音频帧的大小。
- width, height:图像的宽高(只针对视频)。
- key_frame:是否为关键帧(只针对视频) 。
- pict_type:帧类型(只针对视频) 。例如I,P,B。
程序练习
- 打开视频文件
- 显示一些信息
- 输出.h264文件
- 输出解码后的.YUV文件
我这里查阅了一些资料完成了全篇代码的注释,如果有理解有问题的地方,也希望大家指出
另外我把雷老师的课上作业完成在代码中了,比较简单的写文件
#include <stdio.h>
#include <iostream>
using namespace std;
#define __STDC_CONSTANT_MACROS
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
};
//个人定义的宏,方便设置文件路径
#define PATH "Titanic.ts"
#define PATH_TMP "cuc_ieschool.flv"
int main(int argc, char* argv[])
{
AVFormatContext *pFormatCtx;
int i, videoindex;
AVCodecContext *pCodecCtx;
AVCodec *pCodec;
AVFrame *pFrame,*pFrameYUV;
uint8_t *out_buffer;
AVPacket *packet;
int y_size;
int ret, got_picture;
//这个结构体主要用于图像的处理,比如完成图像格式转换、拉伸图像等。具体可以参见雷老师Blog
struct SwsContext *img_convert_ctx;
//输入文件路径
char filepath[]= PATH;
int frame_cnt;
//初始化
av_register_all();//注册相关组件
avformat_network_init();//初始化网络连接
pFormatCtx = avformat_alloc_context();//为AVFormatContext结构分配内存空间
//打开输入视频文件
if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){
printf("Couldn't open input stream.\n");
return -1;
}
//读取一部分视音频数据并且获得一些相关的信息。
if(avformat_find_stream_info(pFormatCtx,NULL)<0){
printf("Couldn't find stream information.\n");
return -1;
}
//这段代码目的是找哪个流是视频流,然后把索引下标赋值给videoindex这个int
//一般其实index=0是视频流,但并不绝对,所以要做这一步
videoindex=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
videoindex=i;
break;
}
if(videoindex==-1){
printf("Didn't find a video stream.\n");
return -1;
}
//这里找到了我们要的视频流的解码器,为引用方便,把它保存入AVCodecContext结构中
pCodecCtx=pFormatCtx->streams[videoindex]->codec;
//查找解码器
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL){
printf("Codec not found.\n");
return -1;
}
//打开解码器
if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){
printf("Could not open codec.\n");
return -1;
}
/*
* 在此处添加输出视频信息的代码
* 取自于pFormatCtx,使用fprintf()
*/
//这里是课上的小实验,我就懒得开文件了,用cout了。。。
cout << "时长:= " <<pFormatCtx->duration << "ms" << endl;
cout << "封装格式:" << pFormatCtx->iformat->name << endl;
cout << "长封装格式:" << pFormatCtx->iformat->long_name << endl;
//访问视频流获取信息
cout << "长:=" << pCodecCtx->height << ",宽:=" << pCodecCtx->width << endl;
/*以下这段代码主要是分配空间,方便后面遍历每一个数据包时的解压和YUV数据的剪裁*/
//为AVFrame分配空间
pFrame=av_frame_alloc();
pFrameYUV=av_frame_alloc();
//av_malloc是对malloc的封装,保证地址对齐
//avpicture_get_size第一个参数指明每一个像素存的数据类型
out_buffer=(uint8_t *)av_malloc(avpicture_get_size(PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));
//把buffer和pFramYUV关联,让每一帧YUV数据data有内存放
avpicture_fill((AVPicture *)pFrameYUV, out_buffer, PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);
//给压缩的数据包分配空间,方便后续使用
packet=(AVPacket *)av_malloc(sizeof(AVPacket));
//Output Info-----------------------------
printf("--------------- File Information ----------------\n");
//dump只是个调试函数,输出文件的音、视频流的基本信息了,帧率、分辨率、音频采样等等
av_dump_format(pFormatCtx,0,filepath,0);
printf("-------------------------------------------------\n");
//初始化用于图像转换的结构体,其中倒数第四个参数表示拉伸图像所用的算法
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
//核心的循环,完成解码
FILE* fp1 = fopen("out.h264", "wb+");
FILE* fp2 = fopen("out.yuv", "wb+");
frame_cnt=0;
//函数读取下一个包
while(av_read_frame(pFormatCtx, packet)>=0){
//如果这个包属于视频
if(packet->stream_index==videoindex){
/*
* 在此处添加输出H264码流的代码
* 取自于packet,使用fwrite()
*/
fwrite(packet->data, packet->size, 1, fp1);
//解码这个包,存入帧内
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if(ret < 0){
printf("Decode Error.\n");
return -1;
}
//由于此时未必完成了一帧的解码,判断一下是否完成了
if(got_picture){
//解码出来的数据在右面可能会有黑边,这和系统硬件相关,该函数就是为了去除这个黑边
//原来保存在pFrame中的数据,经过处理存入了pFrameYUV
sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
pFrameYUV->data, pFrameYUV->linesize);
printf("Decoded frame index: %d\n",frame_cnt);
/*
* 在此处添加输出YUV的代码
* 取自于pFrameYUV,使用fwrite()
*/
//注意这里的大小需要根据YUV420p的知识计算,不能单纯的用sizeof
//一个像素的Y正好占一个字节
fwrite(pFrameYUV->data[0], pCodecCtx->width * pCodecCtx->height, 1, fp2);
fwrite(pFrameYUV->data[1], pCodecCtx->width * pCodecCtx->height / 4, 1, fp2);
fwrite(pFrameYUV->data[2], pCodecCtx->width * pCodecCtx->height / 4, 1, fp2);
frame_cnt++;
}
}
//和av_read_frame相对,释放掉packet中data申请来的空间
av_free_packet(packet);
}
sws_freeContext(img_convert_ctx);
av_frame_free(&pFrameYUV);
av_frame_free(&pFrame);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
fclose(fp1);
fclose(fp2);
return 0;
}