C++ 视频库 ffmpeg 1. 基本概念我们常说的视频文件例如 avi 文件MP4文件等本质上是一种“容器”其内部存放一帧帧的视频信息和音频信息。因此视频文件内部常常包含不止一个“信息流”而是包含一组“信息流”若干视频流和若干音频流。所谓的“信息流”其实就是随时间分布的信息而已。比如视频可以看成是一组随时间分布的“图片”。视频流中的一个数据元通常被称作“一帧 (frame)”每一种视频流都有属于自己的编解码器encoder/decoder在FFmpeg中被简写为codec用于说明该种视频流是如何编码和解码的。数据包 (packets( 则常常指从裸数据帧解析而来的数据片段。总体来说处理音视频流是非常简单的通常包含以下几个步骤step1. 打开音视频文件获取音视频流step2. 从数据流读取数据帧step3. 如果数据帧不完整就回到step2step4. 处理数据帧step5. 回到step2事实上使用 FFmpeg 处理多媒体音视频的基本步骤和上述“伪代码”没有太多不同当然了“step4. 处理数据帧”是一个暧昧的说法毕竟这短短几个字背后的工作量可能非常巨大。本节将尝试使用 FFmpeg 处理一段视音频文件这里所谓的“处理”其实就是将视频分解为若干个 ppm 图片并存储到磁盘。2. 安装sudo apt install ffmpeg3. 作为工具使用3.1. 转换格式ffmpeg -framerate 24 -i input.h264 -c copy output.mp4 ffmpeg -i input.mp4 -vf scale-1:720 -b 1700k -c mpeg4 output_mp4.mp4-vf 是使用过滤器的意思这里使用的过滤器叫scale。-b 是设置比特率不管你size是多少只要比特率一定输出的文件大小就一定。-c 是设定输出视频格式这里使用标准的mp4就行了。-1:720 表示垂直分辨率为720,横向保持横纵比。3.2.截取部分视频ffmpeg -i movie.mp4 -ss 00:00:03 -t 00:00:08 -async 1 cut.mp43.3. 添加字幕ffmpeg -i chamo.srt chamo.ass ffmpeg -i chamo.mp4 -vf asschamo.ass mysubtitledmovie.mp43.4. 图片合成视频ffmpeg -framerate 2 -pattern_type glob -i [your path]/*.jpg -c:v libx264 -r 25 output.mp4-framerate 帧率-i 输入文件ffmpeg -framerate 1 -i images/%03d.jpg -c:v libx264 -crf 17 -pix_fmt yuv420p output.mp4-i images/%03d.jpg: 指定图片的输入目录和文件名格式。注意要求输入图片必须是连续排列的不能有丢帧。4. 作为链接库使用4.1. 打开文件首先我们来看看如何打开一个视音频文件。使用 FFmpeg 之前首先需要注册相关的库这一过程是简单的请参考下面的C语言代码#include libavcodec/avcodec.h #include libavformat/avformat.h #include libswscale/swscale.h int main(int argc, char *argv[]) { if (argc 2) { printf(usage:\n\t %s filename\n, argv[0]); return -1; } 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语言代码可以看出我们将要打开的文件名通过程序的第一个参数 (argv1) 指定avformat_open_input() 函数可以读取文件头信息并将其放在 pctx 中。后面的两个参数用于指定视频文件的格式以及选项配置信息的我们将其设置为 NULLFFmpeg 库将自动探测这些信息。只获取视频文件的头信息是不够的因此需要进一步的探测视频文件的流信息这一步可以通过下面这个函数实现请看相关C语言代码// 进一步探测信息 assert(avformat_find_stream_info(pctx, NULL)0);这个函数主要填充 pctx-streams 成员可以使用下面这个函数显示 FFmpeg 的一些中间过程信息到终端// 显示中间过程信息 av_dump_format(pctx, 0, argv[1], 0);下图是一个中间过程信息实例pctx-streams 本质上是一组指针每一个指针都对应着视频容器中存储的一种流它的 size 等于 pctx-nb_streams所以可以通过遍历对比的方式从这一组流中找到视频流相关的C语言代码可以如下写int i, video_stream -1; for (i0; ipctx-nb_streams; i) { // 查找第一个视频流 if (pctx-streams[i]-codec-codec_typeAVMEDIA_TYPE_VIDEO) { video_stream i; break; } } if (-1video_stream) { printf(no video stream detected\n); return -1; } // pcodec_ctx 指向第一个视频流 AVCodecContext *pcodec_ctx pctx-streams[video_stream]-codec;流信息的编解码器 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(couldnt copy codec context\n); return -1; } // 打开编解码器 if (avcodec_open2(pcodec_ctx, pcodec, NULL) 0) { printf(couldnt open codec\n); return -1; }应注意我们一定不能直接使用视频流的 AVCodecContext所以不得不使用avcodec_copy_context()拷贝了一份上下文。当然了在拷贝之前需要先调用avcodec_alloc_context3()为其分配相应的内存。4.2. 存储数据帧存储数据帧之前肯定需要先分配一块内存这一过程的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() 是类似的应注意避免内存泄漏多重释放等问题。现在我们可以使用avpicture_fill()函数将视频帧数据填充到新分配的 buffer 里了这一过程的C语言代码是简单的avpicture_fill( (AVPicture *)pframe_rgb, buffer, AV_PIX_FMT_RGB24, pcodec_ctx-width, pcodec_ctx-height );终于我们准备好从视频流里读取数据了4.3. 读取数据现在要做的就是从视频流中读取数据到 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 (i5) { save_frame(pframe_rgb, pcodec_ctx-width, pcodec_ctx-height,i); } } av_free_packet(pkt);这一过程的代码虽然稍稍长了点但是很简单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 (y0; yheight; y) fwrite(pframe-data[0]y*pframe-linesize[0], 1, width*3, fp); fclose(fp); }save_frame()函数的C语言代码大都是基础库的使用唯一需要说明的是下面这行代码fprintf(fp, P6\n%d %d\n255\n, width, height);它为 PPM 文件添加了固定的头部信息。4.4. 关闭使用完毕的资源现在文章开头计划的工作完成了可以关闭所有使用完毕的资源了具体的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);4.5. 编译并执行在执行编译生成的C语言程序时在命令行指定视频文件所在的路径我在工程目录里放入了一个名为“test.avi”的视频文件因此可以如下执行程序a.out ./test.avi最终输出如下这说明程序正常运行了查看程序所在目录的确有若干 PPM 文件生成并且可以通过图片浏览器打开参考文献ffmpeg常用操作举例_ziliwangmoe的博客-CSDN博客FFmpeg使用实例详解第一节读取视频文件将其逐帧分解为多张图片 - 刘冲的博客