4

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

 3 years ago
source link: https://blog.popkx.com/FFmpeg%E4%BD%BF%E7%94%A8%E5%AE%9E%E4%BE%8B%E8%AF%A6%E8%A7%A3%E7%AC%AC%E4%B8%80%E8%8A%82-%E8%AF%BB%E5%8F%96%E8%A7%86%E9%A2%91%E6%96%87%E4%BB%B6-%E5%B0%86%E5%85%B6%E9%80%90%E5%B8%A7%E5%88%86%E8%A7%A3%E4%B8%BA%E5%A4%9A%E5%BC%A0%E5%9B%BE%E7%89%87/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

视音频的基本概念

我们常说的视频文件(例如 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 文件

PPM 文件


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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK