使用C语言制作视频播放器(2),将视频拆分成图片组,并存到磁盘

视音频的基本概念

我们常说的视频文件(例如 avi 文件,MP4 文件等)本质上是一种“容器”,其内部存放一帧帧的视频信息和音频信息。因此,视频文件内部常常包含不止一个“信息流”,而是包含一组“信息流”(若干视频流和若干音频流)。

所谓的“信息流”,其实就是随时间分布的信息而已。比如视频可以看成是一组随时间分布的“图片”。

视频流中的一个数据元通常被称作“一帧(frame)”,每一种视频流都有属于自己的编解码器(enCOder/DECoder,在FFmpeg中被简写为 codec),用于说明该种视频流是如何编码和解码的。数据包(packets)则常常指从裸数据帧解析而来的数据片段。

使用C语言制作视频播放器(2),将视频拆分成图片组,并存到磁盘

处理音视频流是非常简单的

总体来说,处理音视频流是非常简单的,通常包含以下几个步骤:

step1. 打开音视频文件,获取音视频流
step2. 从数据流读取数据帧
step3. 如果数据帧不完整,就回到 step2
step4. 处理数据帧
step5. 回到 step2

事实上,使用 FFmpeg 处理多媒体音视频的基本步骤和上述“伪代码”没有太多不同,当然了,“step4. 处理数据帧”是一个暧昧的说法,毕竟这短短几个字背后的工作量可能非常巨大。

本节将尝试使用 FFmpeg 处理一段视音频文件,这里所谓的“处理”,其实就是将视频分解为若干个 ppm 图片,并存储到磁盘。

打开文件

首先,我们来看看如何打开一个视音频文件。使用 FFmpeg 之前,首先需要注册相关的库,这一过程是简单的,请参考下面的C语言代码:

#include <libavcodec>
#include <libavformat>
#include <libswscale>


...
int main(int argc, char *argv[])
{
if (argc < 2){
printf("usage:\\n\\t %s filename\\n", argv[0]);
return -1;
}

av_register_all();
.../<libswscale>/<libavformat>/<libavcodec>
使用C语言制作视频播放器(2),将视频拆分成图片组,并存到磁盘

调用av_register_all()函数

av_register_all()函数可以注册 FFmpeg 中所有可用的文件格式和编解码库 codecs,因为这个函数在项目中只需要也只应该调用一次,所以将其放在 main() 函数中了,这不是必须的,当然也可以将其放在项目中的其他地方。

现在我们可以打开相应的文件了:

AVFormatContext *pctx = NULL;
// 打开文件
if (avformat_open_input(&pctx, argv[1], NULL, NULL)!=0) {
return -1;
}

从这段C语言代码可以看出,我们将要打开的文件名通过程序的第一个参数(argv[1])指定,avformat_open_input() 函数可以读取文件头信息,并将其放在 pctx 中。后面的两个参数用于指定视频文件的格式,以及选项配置信息的,我们将其设置为 NULL,FFmpeg 库将自动探测这些信息。

只获取视频文件的头信息是不够的,因此需要进一步的探测视频文件的流信息,这一步可以通过下面这个函数实现,请看相关C语言代码:

// 进一步探测信息
assert(avformat_find_stream_info(pctx, NULL)>=0);

这个函数主要填充 pctx->streams 成员,可以使用下面这个函数显示 FFmpeg 的一些中间过程信息到终端:

// 显示中间过程信息
av_dump_format(pctx, 0, argv[1], 0);

下图是一个中间过程信息实例:

使用C语言制作视频播放器(2),将视频拆分成图片组,并存到磁盘

中间过程信息实例

pctx->streams 本质上是一组指针,每一个指针都对应着视频容器中存储的一种流,它的 size 等于 pctx->nb_streams,所以可以通过遍历对比的方式从这一组流中找到视频流,相关的C语言代码可以如下写:

 int i, video_stream = -1;
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;/<pctx->
使用C语言制作视频播放器(2),将视频拆分成图片组,并存到磁盘

通过遍历对比的方式从这一组流中找到视频流

流信息的编解码器 codec 就存放在我们称作“codec context(编解码上下文)”中,它包含对应流信息使用的 codec 的所有信息,上述代码的最后定义了pcodec_ctx指针,并让其指向了对打开视频容器中的第一个视频流的 codec 上下文,现在可以根据上下文查找对应视频流的实际编解码器 codec 了,相应的C语言代码可以如下写:

 AVCodec *pcodec = NULL;
// 查找视频流对应的解码器
pcodec = avcodec_find_decoder(pcodec_ctx->codec_id);
if (NULL == pcodec) {
printf("unsupported codec.\\n");
return -1;
}
// 拷贝上下文
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;
}
使用C语言制作视频播放器(2),将视频拆分成图片组,并存到磁盘

根据上下文查找对应视频流的实际编解码器 codec

应注意,我们一定不能直接使用视频流的 AVCodecContext,所以不得不使用 avcodec_copy_context() 拷贝了一份上下文。当然了,在拷贝之前,需要先调用 avcodec_alloc_context3() 为其分配相应的内存。

存储数据帧

存储数据帧之前,肯定需要先分配一块内存,这一过程的C语言代码可以如下写:

 AVFrame *pframe = av_frame_alloc();
AVFrame *pframe_rgb = av_frame_alloc();
assert(pframe && pframe_rgb);

既然我们计划输出 24-bit RGB 格式的 PPM 文件,那么必须先将打开的输入视频文件从它原来的格式转换为 RGB 格式,因此上面的C语言代码还预先分配了额外的一块内存,用于存储转换后的数据。

上面的C语言代码分配的是输出数据的内存,我们还需要分配一块内存供原始数据使用,为此,首先要现知道需要多少内存,这一过程可以调用 avpicture_get_size() 函数得到,相关的C语言代码如下,请看:

 int num_bytes = avpicture_get_size(AV_PIX_FMT_RGB24, 
pcodec_ctx->width, pcodec_ctx->height);
uint8_t *buffer = av_malloc(num_bytes * sizeof(uint8_t));

av_malloc() 函数是 FFmpeg 的内存分配函数,它其实不过是 malloc() 函数的简单封装而已,只不过确保了内存地址对齐以提升程序的效率。使用它和使用 malloc() 是类似的,应注意避免内存泄漏,多重释放等问题。

使用C语言制作视频播放器(2),将视频拆分成图片组,并存到磁盘

应注意避免内存泄漏,多重释放等问题

现在我们可以使用 avpicture_fill() 函数将视频帧数据填充到新分配的 buffer 里了,这一过程的C语言代码是简单的:

avpicture_fill(
(AVPicture *)pframe_rgb,
buffer,
AV_PIX_FMT_RGB24,
pcodec_ctx->width,
pcodec_ctx->height
);

终于,我们准备好从视频流里读取数据了!

读取数据

现在要做的就是从视频流中读取数据到 packet,然后解码成帧,将其转换为我们需要的格式,再保存到磁盘,相应的C语言代码如下,请看:

 int frame_finished;
AVPacket pkt;
// 初始化 sws 上下文,用于转换数据格式
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; // 作为实例,只保存前 5 帧
while (av_read_frame(pctx, &pkt) >= 0) {
if (pkt.stream_index != video_stream) {
continue;
}
avcodec_decode_video2(pcodec_ctx, pframe, &frame_finished, &pkt);
if (!frame_finished)
continue;

sws_scale(sws_ctx, pframe->data, pframe->linesize,
0, pcodec_ctx->height, pframe_rgb->data, pframe_rgb->linesize);
if (++i<=5) {
save_frame(pframe_rgb, pcodec_ctx->width, pcodec_ctx->height,i);
}

}

av_free_packet(&pkt);
使用C语言制作视频播放器(2),将视频拆分成图片组,并存到磁盘

转换格式,保存到磁盘

这一过程的代码虽然稍稍长了点,但是很简单:av_read_frame()函数读取视频流信息,并将其存放到 AVPacket 结构的 pkt 变量中,应注意,我们只需分配 AVPacket 结构体的内存,数据(pkt->data)的内存则由 FFmpeg 在其内部自动分配,不过使用完毕后,要调用 av_free_packet()函数释放。

avcodec_decode_video()函数可以将 packet 转换成 frame,不过,解码一个 packet 不一定能够获得 frame 的全部信息,所以需要借助 frame_finished 标志位用于判断这一过程。

得到一个 frame 后,便可调用 sws_scale() 函数将 frame 从其原始的格式(pctx->pix_fmt)转换到我们期望的 RGB 格式,转换完毕后,就可以调用 save_frame() 函数将其保存到磁盘了。

save_frame()是一个自己定义的函数,它的相关C语言代码可以按照下面这样写,请看:

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); // header

for (y=0; y<height> fwrite(pframe->data[0]+y*pframe->linesize[0], 1, width*3, fp);
fclose(fp);
}/<height>
使用C语言制作视频播放器(2),将视频拆分成图片组,并存到磁盘

save_frame()函数的C语言代码

save_frame()函数的C语言代码大都是基础库的使用,唯一需要说明的是下面这行代码:

fprintf(fp, "P6\\n%d %d\\n255\\n", width, height);

它为 PPM 文件添加了固定的头部信息。

关闭使用完毕的资源

现在文章开头计划的工作完成了,可以关闭所有使用完毕的资源了,具体的C语言代码如下,请看:

 // 释放内存
av_free(buffer);
av_free(pframe_rgb);
av_free(pframe);
// 关闭 codec
avcodec_close(pcodec_ctx);
avcodec_close(pcodec_ctx_orig);
// 关闭打开的文件
avformat_close_input(&pctx);

编译并执行

相应的 FFmpeg 库的编译安装请参考上一节FFmpeg的编译安装 ,编译时应指定 FFmpeg 的头文件以及库所在路径:

$ gcc t.c -I <ffmpeg>/include/ -L <ffmpeg>/lib/ -lavutil -lavformat -lavcodec -lavutil -lm -g -lswscale/<ffmpeg>/<ffmpeg>

在执行编译生成的C语言程序时,在命令行指定视频文件所在的路径,我在工程目录里放入了一个名为“test.avi”的视频文件,因此可以如下执行程序:

$ a.out ./test.avi

最终输出如下:

使用C语言制作视频播放器(2),将视频拆分成图片组,并存到磁盘

输出信息

这说明程序正常运行了,查看程序所在目录,的确有若干 PPM 文件生成,并且可以通过图片浏览器打开:

使用C语言制作视频播放器(2),将视频拆分成图片组,并存到磁盘

PPM 文件,点个关注吧

欢迎在评论区一起讨论,质疑。文章都是手打原创,每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。


分享到:


相關文章: