FFmpeg使用实例详解第一节,读取视频文件,将其逐帧分解为多张图片

视音频的基本概念

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

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

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

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

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

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

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

打开文件

首先,我们来看看如何打开一个视音频文件。使用 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语言代码可以看出,我们将要打开的文件名通过程序的第一个参数(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);

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

中间过程信息实例

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;

流信息的编解码器 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;
    }

应注意,我们一定不能直接使用视频流的 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() 是类似的,应注意避免内存泄漏,多重释放等问题。

现在我们可以使用 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);

这一过程的代码虽然稍稍长了点,但是很简单: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; 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 文件添加了固定的头部信息。

关闭使用完毕的资源

现在文章开头计划的工作完成了,可以关闭所有使用完毕的资源了,具体的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

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

$ a.out ./test.avi

最终输出如下:
输出信息

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


完整代码请参考:
码云Gitee
GitHub

阅读更多:   FFmpeg
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif