VS2019开始第一个FFmpeg测试案例
背景
刚开始接触ffmpeg,也阅读过有关ffmpeg相关的数据,这里看法的是机械工业出版社的《FFmpeg从入门到精通》,迫切想上手第一个实践项目。这里是采用使用FFmpeg官方编译的静态库来实现的。
下载FFmpeg源文件以及动态库
FFmpeg官方有源码和动态库可以分别下载的,源码是我用来分析接口的,此处运行并不需要,我们直接使用lib动态库就好了。这里获取FFmpeg的步骤如下:
- 进入FFmpeg官网 https://ffmpeg.org/ 非常好记。
- 找到下载界面:
这里使用的是Btbn编译的版本
至此我们就获取到了ffmpeg动态库了,将源码和编译过的项目都下载一份下来。在源码的doc/example目录下有很多官方的接口测试案例,值得学习和参考。
搭建VS2019的开发环境(略)
下载安装过程入略过。
创建FFmpeg空项目
-
新建项目
-
这里我们选择控制台项目,使用空项目也是可以的,然后点击下一步。
-
设置项目名称和路径。
-
这就是创建完成了
导入ffmpeg动态库
-
到这一步都很简单,接下来我们要导入ffmpeg的动态库了。
-
在.sln的同级目录下创建一个文件夹,存放有关ffmpeg的动态库。并将ffmpeg中的include文件文件夹和Lib文件夹复制进去。windows开发时Lib目录下只需要.Lib文件即可。
-
将ffmpeg目录下bin下面的动态库复制到.sin的同级目录下,这一步应该是运行时会调用到这些动态库。
-
回到VS2019,右键项目,点击属性调出项目设置窗口。(也可以通过顶部菜单 项目->FFmpegDemo属性调出)。
-
在这里添加外部头文件引用。注意平台区分,这里选择的是全平台。也可以单独设置x64 或者x86。通过在项目组中添加头文件引用判断是否设置成功。
-
只设置了头文件还不够,还需需要设置静态库库文件引用。不然待会代码是运行不了的。然后在输入中添加库的名称。内容如下:
“avcodec.lib;avformat.lib; avutil.lib; avdevice.lib; avfilter.lib;postproc.lib; swresample.lib; swscale.lib”
wobuxiangqi
至此,导入动态库的工作就结束了。
执行测试程序
这里分析ffmpeg源码项目doc/examples目录下的demuxing_decoding.c文件,该文件描述功能是将视频文件中的视频流和音频分别提取出来。但是该测试案例是c平台的项目。如果VS2019 是开启了C项目就能直接运行。这里演示在CPP项目这个部分的代码,同是稍微改动了一些代码开适应CPP平台,同是基于自己的理解加了点注释。
#include <iostream>
#include <stdio.h>
extern "C" {
#include <libavutil/imgutils.h>
#include <libavutil/samplefmt.h>
#include <libavutil/timestamp.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}
/*
这里原av_err2str(ret)宏 在cpp编译环境下是不可用,原因是:
av_err2str被定义为了一个C语言级别的静态内联函数,有数组空间的定义和开销。C++编译时编译器存在内存方面开销和引用的冲突问题,不能编译通过
这里自己定义了一个方便宏av_err2str_cpp
*/
static char av_error[64] = { 0 };
#define av_err2str_cpp(errnum) av_make_error_string(av_error, 64, errnum)
/*
同上
*/
static char av_time_str[64] = { 0 };
#define av_ts2timestr_cpp(ts, tb) av_ts_make_time_string(av_time_str, ts, tb)
static const char* src_filename = NULL;
static const char* video_dst_filename = NULL;
static const char* audio_dst_filename = NULL;
static FILE* video_dst_file = NULL;
static FILE* audio_dst_file = NULL;
static AVFormatContext* fmt_ctx = NULL; //解封装的上下文,很重要的结构体
static AVCodecContext* video_dec_ctx = NULL, * audio_dec_ctx; //视频流和音频流的编码上线文,编解码过程中非常重要的结构体
static int video_stream_idx = -1, audio_stream_idx = -1; //视频流和音频流在文件中所对应的索引号
static AVStream* video_stream = NULL, * audio_stream = NULL; //存放流信息的结构体
static AVFrame* frame = NULL; //存放原始的流数据,可能是音频流可能是视频流,总之是从文件中读取出来的流数据
static AVPacket* pkt = NULL; //存放压缩格式的数据,要么是准备送去解码,要么就是从编码器这种输出的数据
static int video_frame_count = 0, audio_frame_count = 0; //帧数统计
//视频流相关的一些数据
static int width, height; //宽高
static enum AVPixelFormat pix_fmt; //像素格式
static uint8_t* video_dst_data[4] = { NULL };
static int video_dst_linesize[4];
static int video_dst_bufsize;
//将解码之后的数据存到文件中
static int output_video_frame(AVFrame* frame)
{
if (frame->width != width || frame->height != height ||
(AVPixelFormat)frame->format != pix_fmt) {
/* To handle this change, one could call av_image_alloc again and
* decode the following frames into another rawvideo file. */
fprintf(stderr, "Error: Width, height and pixel format have to be "
"constant in a rawvideo file, but the width, height or "
"pixel format of the input video changed:\n"
"old: width = %d, height = %d, format = %s\n"
"new: width = %d, height = %d, format = %s\n",
width, height, av_get_pix_fmt_name(pix_fmt),
frame->width, frame->height,
av_get_pix_fmt_name((AVPixelFormat)frame->format));
return -1;
}
printf("video_frame n:%d coded_n:%d\n", video_frame_count++, frame->coded_picture_number);
/* copy decoded frame to destination buffer:
* this is required since rawvideo expects non aligned data */
/* 不理解直接翻译:将解码后的帧复制到目标缓冲区,这是必需的,因为rawvideo期望非对齐数据 */
av_image_copy(video_dst_data, video_dst_linesize,
(const uint8_t**)(frame->data), frame->linesize,
pix_fmt, width, height);
/* write to rawvideo file */
fwrite(video_dst_data[0], 1, video_dst_bufsize, video_dst_file);
return 0;
}
//将解码之后的数据存到文件中
static int output_audio_frame(AVFrame* frame)
{
size_t unpadded_linesize = (size_t)frame->nb_samples * (size_t)(av_get_bytes_per_sample((AVSampleFormat)frame->format));
printf("audio_frame n:%d nb_samples:%d pts:%s\n",
audio_frame_count++, frame->nb_samples,
av_ts2timestr_cpp(frame->pts, &audio_dec_ctx->time_base));
/* Write the raw audio data samples of the first plane. This works
* fine for packed formats (e.g. AV_SAMPLE_FMT_S16). However,
* most audio decoders output planar audio, which uses a separate
* plane of audio samples for each channel (e.g. AV_SAMPLE_FMT_S16P).
* In other words, this code will write only the first audio channel
* in these cases.
* You should use libswresample or libavfilter to convert the frame
* to packed data. */
/*将音频数据写入文件中,巴拉巴拉 对于AV_SAMPLE_FMT_S16P格式的音频来说,每个声道使用单独的平面空间 阿巴阿巴。
这里只写入第一声道的数据。你应该使用libswresample或者libavfilter去打包这些数据。
意思就是,对于不同的格式数据会采用交枝或者不交枝的存储方式,这里为方便就只存储一个声道的数据。
在这里,不懂的可以去百度搜索一下AVFrame 中data和extended_data的区别。
如果后续有操作,应该使用libswresample or libavfilter去打包这些原始数据。
下面的extended_data 使用data[0]效果也一样。
*/
fwrite(frame->extended_data[0], 1, unpadded_linesize, audio_dst_file);
return 0;
}
//获取音频或者视频的编码信息并初始化编码上下文
static int open_codec_context(int* stream_idx, AVCodecContext** dec_ctx, AVFormatContext* fmt_ctx, AVMediaType type)
{
int ret, stream_index;
AVStream* st = NULL;
const AVCodec* dec = NULL;
//我的理解是查找type类型的流的索引号,这个索引号用来区分是视频流还是音频流还是字幕流……
ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0);
if (ret < 0) {
fprintf(stderr, "Could not find %s stream in input file '%s'\n", av_get_media_type_string(type), src_filename);
return ret;
}
else {
stream_index = ret;
st = fmt_ctx->streams[stream_index];
/* find decoder for the stream */
/* fmt_ctx的codecpar结构体中得到解码信息 */
dec = avcodec_find_decoder((AVCodecID)st->codecpar->codec_id);
if (!dec) {
fprintf(stderr, "Failed to find %s codec\n",
av_get_media_type_string(type));
return AVERROR(EINVAL);
}
/* Allocate a codec context for the decoder */
/* 为解码上下文dec_ctx分配内存 */
*dec_ctx = avcodec_alloc_context3(dec);
if (!*dec_ctx) {
fprintf(stderr, "Failed to allocate the %s codec context\n", av_get_media_type_string(type));
return AVERROR(ENOMEM);
}
/* Copy codec parameters from input stream to output codec context */
/* 将前面获取到的编解码信息复制到dec_ctx中 */
if ((ret = avcodec_parameters_to_context(*dec_ctx, st->codecpar)) < 0) {
fprintf(stderr, "Failed to copy %s codec parameters to decoder context\n", av_get_media_type_string(type));
return ret;
}
/* Init the decoders */
/* 使用dec_ctx初始化编码器 */
if ((ret = avcodec_open2(*dec_ctx, dec, NULL)) < 0) {
fprintf(stderr, "Failed to open %s codec\n",
av_get_media_type_string(type));
return ret;
}
*stream_idx = stream_index;
}
return 0;
}
//解码数据根据编解码上下文区分怎么编码,
static int decode_packet(AVCodecContext* dec, const AVPacket* pkt)
{
int ret = 0;
/*submit the packet to the decoder*/
/*将未解码的压缩数据送进解码器中*/
ret = avcodec_send_packet(dec, pkt);
if (ret < 0) {
fprintf(stderr, "Error submitting a packet for decoding (%s)\n", av_err2str_cpp(ret));
return ret;
}
/*get all the available frames from the decoder */
/*从解码器中拿到有效的数据 */
while (ret >= 0) {
ret = avcodec_receive_frame(dec, frame);
if (ret < 0) {
// those two return values are special and mean there is no output
// frame available, but there were no errors during decoding
// 判断编码结束或者异常
if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
return 0;
fprintf(stderr, "Error during decoding (%s)\n", av_err2str_cpp(ret));
return ret;
}
// write the frame data to output file
if (dec->codec->type == AVMEDIA_TYPE_VIDEO)
ret = output_video_frame(frame);
else
ret = output_audio_frame(frame);
av_frame_unref(frame);
if (ret < 0)
return ret;
}
return 0;
}
int main(int argc, char** argv) {
int ret = 0;
if (argc != 4) {
fprintf(stderr, "usage: %s input_file video_output_file audio_output_file\n"
"API example program to show how to read frames from an input file.\n"
"This program reads frames from a file, decodes them, and writes decoded\n"
"video frames to a rawvideo file named video_output_file, and decoded\n"
"audio frames to a rawaudio file named audio_output_file.\n",
argv[0]);
exit(1);
}
src_filename = argv[1];
video_dst_filename = argv[2];
audio_dst_filename = argv[3];
/* open input file, and allocate format context */
/* 绑定文件路径,并为 分配内存*/
if (avformat_open_input(&fmt_ctx, src_filename, NULL, NULL) < 0) {
fprintf(stderr, "Could not open source file %s\n", src_filename);
exit(1);
}
/* retrieve stream information */
/* 检索文件文件中是否有媒体流 */
if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
fprintf(stderr, "Could not find stream information\n");
exit(1);
}
if (open_codec_context(&video_stream_idx, &video_dec_ctx, fmt_ctx, AVMEDIA_TYPE_VIDEO) >= 0) {
video_stream = fmt_ctx->streams[video_stream_idx];
fopen_s(&video_dst_file, video_dst_filename, "wb");
if (!video_dst_file) {
fprintf(stderr, "Could not open destination file %s\n", video_dst_filename);
ret = 1;
goto end;
}
/* allocate image where the decoded image will be put */
width = video_dec_ctx->width;
height = video_dec_ctx->height;
pix_fmt = video_dec_ctx->pix_fmt;
ret = av_image_alloc(video_dst_data, video_dst_linesize,
width, height, pix_fmt, 1);
if (ret < 0) {
fprintf(stderr, "Could not allocate raw video buffer\n");
goto end;
}
video_dst_bufsize = ret;
}
if (open_codec_context(&audio_stream_idx, &audio_dec_ctx, fmt_ctx, AVMEDIA_TYPE_AUDIO) >= 0) {
audio_stream = fmt_ctx->streams[audio_stream_idx];
fopen_s(&audio_dst_file, audio_dst_filename, "wb");
if (!audio_dst_file) {
fprintf(stderr, "Could not open destination file %s\n", audio_dst_filename);
ret = 1;
goto end;
}
}
/* dump input information to stderr */
/* 打印信息 */
av_dump_format(fmt_ctx, 0, src_filename, 0);
//判断有没有视频流或者音频流
if (!audio_stream && !video_stream) {
fprintf(stderr, "Could not find audio or video stream in the input, aborting\n");
ret = 1;
goto end;
}
frame = av_frame_alloc();
if (!frame) {
fprintf(stderr, "Could not allocate frame\n");
ret = AVERROR(ENOMEM);
goto end;
}
pkt = av_packet_alloc();
if (!pkt) {
fprintf(stderr, "Could not allocate packet\n");
ret = AVERROR(ENOMEM);
goto end;
}
if (video_stream)
printf("Demuxing video from file '%s' into '%s'\n", src_filename, video_dst_filename);
if (audio_stream)
printf("Demuxing audio from file '%s' into '%s'\n", src_filename, audio_dst_filename);
/* read frames from the file */
/* 现在开始从文件中读取流数据放在pkt中 */
while (av_read_frame(fmt_ctx, pkt) >= 0) {
// check if the packet belongs to a stream we are interested in, otherwise
// skip it
if (pkt->stream_index == video_stream_idx)
ret = decode_packet(video_dec_ctx, pkt);
else if (pkt->stream_index == audio_stream_idx)
ret = decode_packet(audio_dec_ctx, pkt);
av_packet_unref(pkt);
if (ret < 0)
break;
}
end:
return 0;
}
这里将会遇到几个警告,有关C26812警告是c++11引进的一个警告,具体原因可参看https://blog.csdn.net/weixin_44793491/article/details/108064051