学习雷神的博客,向雷神致敬~
看了雷神的小学期视频课,在Github上下载了simplest_ffmpeg_player的代码,为代码加上了注释,作为留存。
2019.07.16
视频中的前置知识点
simple_ffmpeg_decoder.cpp注释
simple_ffmpeg_decoder_pure.cpp注释
《基于 FFmpeg + SDL 的视频播放器的制作》课程的视频
FFmpeg Documentation
FFmpeg源代码简单分析
AVFormatContext:封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息
AVInputFormat:每种封装格式对应一个该结构体
AVStream:视频文件每个视频(音频)流对应一个该结构体
AVCodecContext:编码器上下文结构体,保存了视频(音频)编解码相关信息
AVCodec:每种视频(音频)编解码器对应一个该结构体
AVPacket:存储一帧压缩编码数据
AVFrame:存储一帧解码后像素(采样)数据
AVFormatContext
AVIputFormat
AVStream
AVCodecContext
AVCodec
/**
* 最简单的基于FFmpeg的视频解码器
* Simplest FFmpeg Decoder
*
* 雷霄骅 Lei Xiaohua
* [email protected]
* 中国传媒大学/数字电视技术
* Communication University of China / Digital TV Technology
* http://blog.csdn.net/leixiaohua1020
*
*
* 本程序实现了视频文件解码为YUV数据。它使用了libavcodec和
* libavformat。是最简单的FFmpeg视频解码方面的教程。
* 通过学习本例子可以了解FFmpeg的解码流程。
* This software is a simplest decoder based on FFmpeg.
* It decodes video to YUV pixel data.
* It uses libavcodec and libavformat.
* Suitable for beginner of FFmpeg.
*
*/
#include
#define __STDC_CONSTANT_MACROS
#ifdef _WIN32
//Windows
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include
#include
#include
#include
#ifdef __cplusplus
};
#endif
#endif
/**
* 将视频解封装、解码,转换为yuv格式
**/
int main(int argc, char* argv[])
{
// 封装格式上下文的结构体,也是统领全局的结构体,保存了视频文件封装格式的相关信息
AVFormatContext *pFormatCtx;
// 视频流在文件中的位置
int i, videoindex;
// 编码器上下文结构体,保存了视频(音频)编解码相关信息
AVCodecContext *pCodecCtx;
// 每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体
AVCodec *pCodec;
// 存储一帧解码后像素(采样)数据
AVFrame *pFrame,*pFrameYUV;
//
unsigned char *out_buffer;
// 存储一帧压缩编码数据
AVPacket *packet;
// width×height,用于计算YUV数据分布
int y_size;
// 视频是否解码成功的返回
int ret, got_picture;
// libswsscale 上下文
struct SwsContext *img_convert_ctx;
// 输入文件
char filepath[]="Titanic.mkv";
// 输出文件
FILE *fp_yuv=fopen("output.yuv","wb+");
// 注册复用器,编码器等(参考FFmpeg解码流程图)
av_register_all();
avformat_network_init();
pFormatCtx = avformat_alloc_context();
// 打开多媒体数据并且获得一些相关的信息(参考FFmpeg解码流程图)
if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){
printf("Couldn't open input stream.\n");
return -1;
}
// 读取一部分视音频数据并且获得一些相关的信息(参考FFmpeg解码流程图)
if(avformat_find_stream_info(pFormatCtx,NULL)<0){
printf("Couldn't find stream information.\n");
return -1;
}
// 每个视频文件中有多个流(视频流、音频流、字幕流等,而且可有多个),循环遍历找到视频流
// 判断方式:AVFormatContext->AVStream->AVCodecContext->codec_type是否为AVMEDIA_TYPE_VIDEO
videoindex=-1;
for(i=0; inb_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;
// 用于查找FFmpeg的解码器(参考FFmpeg解码流程图)
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL){
printf("Codec not found.\n");
return -1;
}
// (参考FFmpeg解码流程图)
if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){
printf("Could not open codec.\n");
return -1;
}
// 创建一个AVFrame,用来存放解码后的一帧的数据
pFrame=av_frame_alloc();
pFrameYUV=av_frame_alloc();
// av_image_get_buffer_size:返回使用给定参数存储图像所需的数据量的字节大小
out_buffer=(unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height,1));
// 根据指定的图像参数和提供的数组设置数据指针和线条(data pointers and linesizes)
av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize,out_buffer,
AV_PIX_FMT_YUV420P,pCodecCtx->width, pCodecCtx->height,1);
// 创建一个AVPacket,用来存放下面循环获取到的未解码帧
packet=(AVPacket *)av_malloc(sizeof(AVPacket));
//Output Info-----------------------------
printf("--------------- File Information ----------------\n");
av_dump_format(pFormatCtx,0,filepath,0);
printf("-------------------------------------------------\n");
// sws_getContext():初始化一个SwsContext
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
// 循环读取帧数据
while(av_read_frame(pFormatCtx, packet)>=0){
// 取出视频流,
if(packet->stream_index==videoindex){
// 解码一帧视频数据:输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if(ret < 0){
printf("Decode Error.\n");
return -1;
}
if(got_picture){
// sws_scale():处理图像数据,用于转换像素
sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
pFrameYUV->data, pFrameYUV->linesize);
// 根据YUV数据格式,分离Y、U、V数据
// 如果视频帧的宽和高分别为w和h,那么一帧YUV420P像素数据一共占用w*h*3/2 Byte的数据
// 其中前w*h Byte存储Y,接着的w*h*1/4 Byte存储U,最后w*h*1/4 Byte存储V
y_size=pCodecCtx->width*pCodecCtx->height;
fwrite(pFrameYUV->data[0],1,y_size,fp_yuv); //Y
fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv); //U
fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv); //V
printf("Succeed to decode 1 frame!\n");
}
}
av_free_packet(packet);
}
//flush decoder
//FIX: Flush Frames remained in Codec
while (1) {
// 解码一帧视频数据:输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if (ret < 0)
break;
if (!got_picture)
break;
// // sws_scale():处理图像数据,用于转换像素
sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
pFrameYUV->data, pFrameYUV->linesize);
int y_size=pCodecCtx->width*pCodecCtx->height;
fwrite(pFrameYUV->data[0],1,y_size,fp_yuv); //Y
fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv); //U
fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv); //V
printf("Flush Decoder: Succeed to decode 1 frame!\n");
}
// sws_freeContext():释放一个SwsContext
sws_freeContext(img_convert_ctx);
// close and free
fclose(fp_yuv);
av_frame_free(&pFrameYUV);
av_frame_free(&pFrame);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
return 0;
}
/**
* 最简单的基于FFmpeg的视频解码器(纯净版)
* Simplest FFmpeg Decoder Pure
*
* 雷霄骅 Lei Xiaohua
* [email protected]
* 中国传媒大学/数字电视技术
* Communication University of China / Digital TV Technology
* http://blog.csdn.net/leixiaohua1020
*
*
* 本程序实现了视频码流(支持HEVC,H.264,MPEG2等)解码为YUV数据。
* 它仅仅使用了libavcodec(而没有使用libavformat)。
* 是最简单的FFmpeg视频解码方面的教程。
* 通过学习本例子可以了解FFmpeg的解码流程。
* This software is a simplest decoder based on FFmpeg.
* It decode bitstreams to YUV pixel data.
* It just use libavcodec (do not contains libavformat).
* Suitable for beginner of FFmpeg.
*/
#include
#define __STDC_CONSTANT_MACROS
#ifdef _WIN32
//Windows
extern "C"
{
#include "libavcodec/avcodec.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include
#ifdef __cplusplus
};
#endif
#endif
//test different codec
#define TEST_H264 1
#define TEST_HEVC 0
int main(int argc, char* argv[])
{
// 每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体
AVCodec *pCodec;
// 编码器上下文结构体,保存了视频(音频)编解码相关信息
AVCodecContext *pCodecCtx= NULL;
// 保存了当前帧的信息,包括offset、dts、pts、宽高等
AVCodecParserContext *pCodecParserCtx=NULL;
FILE *fp_in;
FILE *fp_out;
// 存储一帧解码后像素(采样)数据
AVFrame *pFrame;
const int in_buffer_size=4096;
// FF_INPUT_BUFFER_PADDING_SIZE:在输入比特流的末尾用于解码的额外分配字节的所需数量。
// 这主要是因为一些优化的比特流读取器一次读取32位或64位并且可以读取结束。
// 注意:如果附加字节的前23位不为0,则损坏的MPEG比特流可能导致过度读取和段错误。
unsigned char in_buffer[in_buffer_size + FF_INPUT_BUFFER_PADDING_SIZE]={0};
unsigned char *cur_ptr;
int cur_size;
// 存储一帧压缩编码数据
AVPacket packet;
// 视频是否解码成功的返回
int ret, got_picture;
// hevc h264 m2v -> yuv
#if TEST_HEVC
enum AVCodecID codec_id=AV_CODEC_ID_HEVC;
char filepath_in[]="bigbuckbunny_480x272.hevc";
#elif TEST_H264
AVCodecID codec_id=AV_CODEC_ID_H264;
char filepath_in[]="bigbuckbunny_480x272.h264";
#else
AVCodecID codec_id=AV_CODEC_ID_MPEG2VIDEO;
char filepath_in[]="bigbuckbunny_480x272.m2v";
#endif
char filepath_out[]="bigbuckbunny_480x272.yuv";
int first_time=1;
//av_log_set_level(AV_LOG_DEBUG);
// 注册复用器,编码器等(参考FFmpeg解码流程图)
avcodec_register_all();
// 用于查找FFmpeg的解码器(参考FFmpeg解码流程图)
pCodec = avcodec_find_decoder(codec_id);
if (!pCodec) {
printf("Codec not found\n");
return -1;
}
// 创建AVCodecContext结构体
pCodecCtx = avcodec_alloc_context3(pCodec);
if (!pCodecCtx){
printf("Could not allocate video codec context\n");
return -1;
}
// 初始化AVCodecParserContext。其参数是codec_id,所以同时只能解析一种
// AVCodecParser用于解析输入的数据流并把它们分成一帧一帧的压缩编码数据。
// 比较形象的说法就是把长长的一段连续的数据“切割”成一段段的数据。
// 核心函数是av_parser_parse2()
pCodecParserCtx=av_parser_init(codec_id);
if (!pCodecParserCtx){
printf("Could not allocate video parser context\n");
return -1;
}
//if(pCodec->capabilities&CODEC_CAP_TRUNCATED)
// pCodecCtx->flags|= CODEC_FLAG_TRUNCATED;
// 使用给定的AVCodec初始化AVCodecContext;
// 在使用这个函数之前需要使用avcodec_alloc_context3()分配的context
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
printf("Could not open codec\n");
return -1;
}
//Input File
fp_in = fopen(filepath_in, "rb");
if (!fp_in) {
printf("Could not open input stream\n");
return -1;
}
//Output File
fp_out = fopen(filepath_out, "wb");
if (!fp_out) {
printf("Could not open output YUV file\n");
return -1;
}
pFrame = av_frame_alloc();
// 把packet的参数设为默认值,要求packet的内存已经分配好了
av_init_packet(&packet);
while (1) {
// 获取视频文件的总长度
cur_size = fread(in_buffer, 1, in_buffer_size, fp_in);
if (cur_size == 0)
break;
cur_ptr=in_buffer;
while (cur_size>0){
/**
* 解析数据获得一个Packet, 从输入的数据流中分离出一帧一帧的压缩编码数据
* Parse a packet.
*
* @param s parser context.
* @param avctx codec context.
* @param poutbuf set to pointer to parsed buffer or NULL if not yet finished.
* @param poutbuf_size set to size of parsed buffer or zero if not yet finished.
* @param buf input buffer.
* @param buf_size input length, to signal EOF, this should be 0 (so that the last frame can be output).
* @param pts input presentation timestamp.
* @param dts input decoding timestamp.
* @param pos input byte position in stream.
* @return the number of bytes of the input bitstream used.
*
* Example:
* @code
* while(in_len){
* len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
* in_data, in_len,
* pts, dts, pos);
* in_data += len;
* in_len -= len;
*
* if(size)
* decode_frame(data, size);
* }
* @endcode
*
* 其中poutbuf指向解析后输出的压缩编码数据帧,buf指向输入的压缩编码数据。
* 如果函数执行完后输出数据为空(poutbuf_size为0),则代表解析还没有完成,还需要再次调用av_parser_parse2()解析一部分数据才可以得到解析后的数据帧。
* 当函数执行完后输出数据不为空的时候,代表解析完成,可以将poutbuf中的这帧数据取出来做后续处理。
*/
int len = av_parser_parse2(
pCodecParserCtx, pCodecCtx,
&packet.data, &packet.size,
cur_ptr , cur_size ,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, AV_NOPTS_VALUE);
cur_ptr += len;
cur_size -= len;
// 如果函数执行完后输出数据为空(poutbuf_size为0),则代表解析还没有完成,还需要再次调用av_parser_parse2()解析一部分数据才可以得到解析后的数据帧。
if(packet.size==0)
continue;
//Some Info from AVCodecParserContext
printf("[Packet]Size:%6d\t",packet.size);
switch(pCodecParserCtx->pict_type){
case AV_PICTURE_TYPE_I: printf("Type:I\t");break;
case AV_PICTURE_TYPE_P: printf("Type:P\t");break;
case AV_PICTURE_TYPE_B: printf("Type:B\t");break;
default: printf("Type:Other\t");break;
}
printf("Number:%4d\n",pCodecParserCtx->output_picture_number);
// 解码一帧视频数据:输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &packet);
if (ret < 0) {
printf("Decode Error.\n");
return ret;
}
if (got_picture) {
if(first_time){
printf("\nCodec Full Name:%s\n",pCodecCtx->codec->long_name);
printf("width:%d\nheight:%d\n\n",pCodecCtx->width,pCodecCtx->height);
first_time=0;
}
//Y, U, V
for(int i=0;iheight;i++){
fwrite(pFrame->data[0]+pFrame->linesize[0]*i,1,pFrame->width,fp_out);
}
for(int i=0;iheight/2;i++){
fwrite(pFrame->data[1]+pFrame->linesize[1]*i,1,pFrame->width/2,fp_out);
}
for(int i=0;iheight/2;i++){
fwrite(pFrame->data[2]+pFrame->linesize[2]*i,1,pFrame->width/2,fp_out);
}
printf("Succeed to decode 1 frame!\n");
}
}
}
//Flush Decoder
packet.data = NULL;
packet.size = 0;
while(1){
// 解码一帧视频数据:输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &packet);
if (ret < 0) {
printf("Decode Error.\n");
return ret;
}
if (!got_picture){
break;
}else {
//Y, U, V
for(int i=0;iheight;i++){
fwrite(pFrame->data[0]+pFrame->linesize[0]*i,1,pFrame->width,fp_out);
}
for(int i=0;iheight/2;i++){
fwrite(pFrame->data[1]+pFrame->linesize[1]*i,1,pFrame->width/2,fp_out);
}
for(int i=0;iheight/2;i++){
fwrite(pFrame->data[2]+pFrame->linesize[2]*i,1,pFrame->width/2,fp_out);
}
printf("Flush Decoder: Succeed to decode 1 frame!\n");
}
}
// close and free
fclose(fp_in);
fclose(fp_out);
av_parser_close(pCodecParserCtx);
av_frame_free(&pFrame);
avcodec_close(pCodecCtx);
av_free(pCodecCtx);
return 0;
}