VS2019开始第一个FFmpeg测试案例

本文详细介绍了如何在Visual Studio 2019中配置FFmpeg开发环境,从下载FFmpeg动态库,创建控制台项目,导入库文件,设置头文件和库引用,到最后执行FFmpeg的解码测试程序。通过分析源码中的demuxing_decoding.c文件,展示了如何解码视频和音频帧并将其写入文件。
摘要由CSDN通过智能技术生成

背景

刚开始接触ffmpeg,也阅读过有关ffmpeg相关的数据,这里看法的是机械工业出版社的《FFmpeg从入门到精通》,迫切想上手第一个实践项目。这里是采用使用FFmpeg官方编译的静态库来实现的。

下载FFmpeg源文件以及动态库

FFmpeg官方有源码和动态库可以分别下载的,源码是我用来分析接口的,此处运行并不需要,我们直接使用lib动态库就好了。这里获取FFmpeg的步骤如下:

  1. 进入FFmpeg官网 https://ffmpeg.org/ 非常好记。
  2. 找到下载界面:在这里插入图片描述
    这里使用的是Btbn编译的版本在这里插入图片描述在这里插入图片描述在这里插入图片描述

至此我们就获取到了ffmpeg动态库了,将源码和编译过的项目都下载一份下来。在源码的doc/example目录下有很多官方的接口测试案例,值得学习和参考。

搭建VS2019的开发环境(略)

下载安装过程入略过。

创建FFmpeg空项目

  1. 新建项目
    在这里插入图片描述

  2. 这里我们选择控制台项目,使用空项目也是可以的,然后点击下一步。
    在这里插入图片描述

  3. 设置项目名称和路径。在这里插入图片描述

  4. 这就是创建完成了
    在这里插入图片描述

导入ffmpeg动态库

  1. 到这一步都很简单,接下来我们要导入ffmpeg的动态库了。

  2. 在.sln的同级目录下创建一个文件夹,存放有关ffmpeg的动态库。并将ffmpeg中的include文件文件夹和Lib文件夹复制进去。windows开发时Lib目录下只需要.Lib文件即可。
    在这里插入图片描述

  3. 将ffmpeg目录下bin下面的动态库复制到.sin的同级目录下,这一步应该是运行时会调用到这些动态库。
    在这里插入图片描述

  4. 回到VS2019,右键项目,点击属性调出项目设置窗口。(也可以通过顶部菜单 项目->FFmpegDemo属性调出)。
    在这里插入图片描述

  5. 在这里添加外部头文件引用。注意平台区分,这里选择的是全平台。也可以单独设置x64 或者x86。通过在项目组中添加头文件引用判断是否设置成功。
    在这里插入图片描述
    在这里插入图片描述

  6. 只设置了头文件还不够,还需需要设置静态库库文件引用。不然待会代码是运行不了的。然后在输入中添加库的名称。内容如下:
    “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

在这里插入图片描述

测试结果验证

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Griza_J

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值