思路
获得路径 - > 解封装 -> 获取视频流的信息 -> 解编码 -> YUV420转RGB -> 以ppm格式存放到文件里。
伪代码
step1. 注册
step2. 打开音视频文件
step3. 获取音视频流
step4. 查找视频类型得AVMEDIA_TYPE_VIDEO
step5. 查找解码器
step6. 拷贝视频流得AVCodecContext
step7. 打开解码器
step8. 获得视频流得总共大小并且申请堆空间
step9. 初始化YUV转RGB
step10. 创建循环读取视频流一帧数据并且判断是否已经是视频流最后一帧
{
解码一帧数据
正式处理YUV转RGB数据类型
保存文件为PPM格式
}
step11. 释放空间,关闭音视频文件
C语言写的全部源代码如下
#include
#include
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/pixfmt.h"
#include "libswscale/swscale.h"
void save_frame(AVFrame *pframe, int width, int height, int iframe);
int main(int argc, char *argv[])
{
char *file_path = "/root/vidoLean/xingJiDaZhan.mp4";
const int frameNumber = 100;
av_register_all();
AVFormatContext *pctx = NULL;
if (avformat_open_input(&pctx, file_path, NULL, NULL)!=0)
{
return -1;
}
assert(avformat_find_stream_info(pctx, NULL)>=0);
av_dump_format(pctx, 0, file_path, 0);
int video_stream = -1;
int i= 0;
for (i=0; i<pctx->nb_streams; i++)
{
// 查找第一个视频流
if(pctx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO)
{
video_stream =i;
break;
}
}
if (-1==video_stream)
{
printf("no video stream detected\n");
return -1;
}
// pcodec_ctx 指向第一个视频流
AVCodecContext *pcodec_ctx =
pctx->streams[video_stream]->codec;
AVCodec *pcodec = NULL;
// 查找视频流对应的解码器
pcodec = avcodec_find_decoder(pcodec_ctx->codec_id);
if (NULL == pcodec)
{
printf("unsupported codec.\n");
return -1;
}
//因为一定不能直接使用视频流的AVCodecContext,所以要拷贝AVCodecContext上下文
AVCodecContext *pcodec_ctx_orig =
avcodec_alloc_context3(pcodec);
if (avcodec_copy_context(pcodec_ctx_orig, pcodec_ctx) != 0)
{
printf("couldn't copy codec context\n");
return -1;
}
// 打开编解码器
if (avcodec_open2(pcodec_ctx, pcodec, NULL) < 0)
{
printf("couldn't open codec\n");
return -1;
}
AVFrame *pframe = av_frame_alloc();
AVFrame *pframe_rgb = av_frame_alloc();
assert(pframe && pframe_rgb);
//得到原始视频流数据得总共大小
int num_bytes = avpicture_get_size(AV_PIX_FMT_RGB24,
pcodec_ctx->width, pcodec_ctx->height);
/*av_malloc() 函数是 FFmpeg 的内存分配函数,
它其实不过是 malloc() 函数的简单封装而已,只不过确保了内存地址对齐以提升程序的效率。
使用它和使用 malloc() 是类似的,应注意避免内存泄漏,多重释放等问题。*/
uint8_t *buffer = av_malloc(num_bytes * sizeof(uint8_t));
//将视频帧数据填充到新分配的 buffer 里了
avpicture_fill(
(AVPicture *)pframe_rgb,
buffer,
AV_PIX_FMT_RGB24,
pcodec_ctx->width,
pcodec_ctx->height
);
int frame_finished;
AVPacket pkt;
//初始化 sws 上下文,用于YUV转换rgb转换。因为视频流原始得数据是yuv,ppm数据是rgb。
struct SwsContext *sws_ctx = sws_getContext(
pcodec_ctx->width,
pcodec_ctx->height,
pcodec_ctx->pix_fmt,
pcodec_ctx->width,
pcodec_ctx->height,
AV_PIX_FMT_RGB24,
SWS_BILINEAR,
NULL,
NULL,
NULL
);
i = 0;
//av_read_frame()读取一帧数据
while (av_read_frame(pctx, &pkt) >= 0)
{
if (pkt.stream_index != video_stream)
{
continue;
}
//解码一帧数据,解码后得数据给pframe,压缩数据给pkt
avcodec_decode_video2(pcodec_ctx, pframe, &frame_finished, &pkt);
if (!frame_finished)
continue;
//将 frame 从其原始的格式(pctx->pix_fmt)转换到我们期望的 RGB 格式
sws_scale(sws_ctx, pframe->data, pframe->linesize,
0, pcodec_ctx->height, pframe_rgb->data, pframe_rgb->linesize);
if (++i<=frameNumber)
{
save_frame(pframe_rgb, pcodec_ctx->width, pcodec_ctx->height,i); //保存ppm图片文件
}
}
av_free_packet(&pkt);
// 释放内存
av_free(buffer);
av_free(pframe_rgb);
av_free(pframe);
// 关闭 codec
avcodec_close(pcodec_ctx);
avcodec_close(pcodec_ctx_orig);
// 关闭打开的文件
avformat_close_input(&pctx);
return 0;
}
void save_frame(AVFrame *pframe, int width, int height, int iframe)
{
char filename[32];
int y;
sprintf(filename, "frame%d.ppm", iframe);
FILE *fp = fopen(filename, "w+");
assert(fp!=NULL);
fprintf(fp, "P6\n%d %d\n255\n", width, height); // PPM 文件添加了固定的头部信息。
for (y=0; y<height; y++)
fwrite(pframe->data[0]+y*pframe->linesize[0], 1, width*3, fp); //ppm存储格式
fclose(fp);
}
av_register_all()
实现 解码器decoder 编码器encoder 编解码器encdec 硬件加速器hwaccel 分析器parser 的注册,实现复用器muxer 和解复用器 demuxer 的注册。
avformat_alloc_context()
该函数用于分配空间创建一个AVFormatContext对象,并且强调使用avformat_free_context方法来清理并释放该对象的空间。
avformat_open_input()
输入输出结构体AVIOContext的初始化;
输入数据的协议(例如RTMP,或者file)的识别(通过一套评分机制):1判断文件名的后缀 2读取文件头的数据进行比对;
使用获得最高分的文件协议对应的URLProtocol,通过函数指针的方式,与FFMPEG连接(非专业用词);
剩下的就是调用该URLProtocol的函数进行open,read等操作了
avformat_find_stream_info()
该函数可以读取一部分音视频数据并且获得一些相关的信息。
该函数主要用于给每个媒体流(音频/视频)的AVStream结构体赋值。大致理解一下这个函数的代码,会发现它其实已经实现了解码器的查找,解码器的打开,视音频帧的读取,视音频帧的解码等工作。换句话说,该函数实际上已经“走通”的解码的整个流程。
avcodec_find_decoder()
函数的参数是一个解码器的ID,返回查找到的解码器(没有找到就返回NULL)。
函数参数为枚举类型。通过id来循环查找相应的解码器id类型的数字。
avcodec_open2()
打开该解码器。用于初始化一个视音频编解码器的AVCodecContext。
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)
成功后返回SwsContext 类型的结构体。
参数1:被转换源的宽
参数2:被转换源的高
参数3:被转换源的格式,eg:YUV、RGB……(枚举格式,也可以直接用枚举的代号表示eg:AV_PIX_FMT_YUV420P这些枚举的格式在libavutil/pixfmt.h中列出)
参数4:转换后指定的宽
参数5:转换后指定的高
参数6:转换后指定的格式同参数3的格式
参数7:转换所使用的算法,
参数8:NULL
参数9:NULL
参数10:NULL
转换所用的算法在libswscale/swscale.h中枚举
avpicture_fill(AVPicture *picture, const uint8_t *ptr,
enum AVPixelFormat pix_fmt, int width, int heig)
avpicture_fill函数将ptr指向的数据填充到picture内,但并没有拷贝,只是将picture结构内的data指针指向了ptr的数据(详解:http://www.voidcn.com/article/p-aquvaett-zg.html)
avpicture_get_size()
这里FFmpeg会帮我们计算这个格式的图片,需要多少字节来存储。联系上下文,详细解释如下地址:http://blog.sina.com.cn/s/blog_825ad93f0102yvou.html
av_new_packet(); //分配packet的数据
AVPacket保存的是解码前的数据,也就是压缩后的数据。该结构本身不直接包含数据,其有一个指向数据域的指针,FFmpeg中很多的数据结构都使用这种方法来管理数据。
av_dump_format()
作为打印程序调试用的。
av_read_frame()
作用是读取码流中的音频若干帧或者视频一帧。例如,解码视频的时候,每解码一个视频帧,需要先调用 av_read_frame()获得一帧视频的压缩数据,然后才能对该数据进行解码(例如H.264中一帧压缩数据通常对应一个NAL)。
avcodec_decode_video2()
ffmpeg中的avcodec_decode_video2()的作用是解码一帧视频数据。输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame。
sws_scale()
函数主要是用来做视频像素格式和分辨率的转换,其优势在于:可以在同一个函数里实现:1.图像色彩空间转换, 2:分辨率缩放,3:前后图像滤波处理。不足之处在于:效率相对较低,不如libyuv或shader
sws_scale()和sws_getContext()区别是什么?
答:sws_getContext()是初始化转换。sws_scale()是处理具体得转换内容。