ffmpeg开发教程(4) : 第一个简单例子

上篇提到FFmpeg解码的一个基本流程:

本篇为一个简单的例子,读取一个视频文件,比如bunny.mp4 , 读取视频的基本信息,然后解码该视频,并把前500帧的图像存储到硬盘。

C++中调用FFmpeg API

之前说过FFmpeg API是以C接口提供,在C++调用,需要说明调用的FFmpeg接口为C接口,编译链接时才能正确链接。

ffmpeg-tutorialtutorial.hpp中我们使用extern “C”  标明ffmpeg的引用为C接口

#if defined (__cplusplus)
extern "C" {
#endif
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <cstdio>
#if defined (__cplusplus)
}

定义好头文件中ffmpeg引用后,我们就可以使用ffmpeg 函数来操作视频文件。

读取视频文件

AVFormatContext *pFormatContext = avformat_alloc_context();
    // Open video file
    if (avformat_open_input(&pFormatContext, argv[1], nullptr, nullptr) != 0)
        return -1; // Couldn't open file

    // Retrieve stream information
    if (avformat_find_stream_info(pFormatContext, nullptr) < 0)
        return -1; // Couldn't find stream information

    // Dump information about file onto standard error
    av_dump_format(pFormatContext, 0, argv[1], 0);

argv[1] 命令行参数指明视频文件名,比如 bunny.mp4, AVFormatContext 为视频格式数据结构(帮助记忆,可以把比AVFormatContext看做文件指针),保存了该视频的一些参数,。如视频,音频参数,编解码格式,片场等。av_dump_format 显示了这些参数,比如bunny.mp4的参数显示如下:

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from './bunny.mp4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2010-01-10T08:29:06.000000Z
  Duration: 00:09:56.47, start: 0.000000, bitrate: 2119 kb/s
    Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 125 kb/s (default)
    Metadata:
      creation_time   : 2010-01-10T08:29:06.000000Z
      handler_name    : (C) 2007 Google Inc. v08.13.2007.
    Stream #0:1(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 1991 kb/s, 24 fps, 24 tbr, 24k tbn, 48 tbc (default)
    Metadata:
      creation_time   : 2010-01-10T08:29:06.000000Z
      handler_name    : (C) 2007 Google Inc. v08.13.2007.

查找视频流

一个视频文件比如(mp4, avi)中包含了多个和多种数据流,有的为音频数据,有的为视频数据或是字幕数据,或者有多个视频,音频,字幕数据流。一般来说只包含一个视频,下面代码查找第一个视频数据流。视频数据流的类型为AVMEDIA_TYPE_VIDEO

int videoStream = -1;
    AVCodecParameters *pCodecParameters = nullptr;
    for (int i = 0; i < pFormatContext->nb_streams; i++) {
        pCodecParameters = pFormatContext->streams[i]->codecpar;
        if (pCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStream = i;
            break;
        }
    }

查找并初始化视频解码器

找到视频数据流后,就需要对应的解码器来解压视频数据,AVCodec,AVCodecContext 为对于的解码器的数据结构。

// Find the decoder for the video stream
    AVCodec *pCodec = avcodec_find_decoder(pCodecParameters->codec_id);
    if (pCodec == nullptr) {
        fprintf(stderr, "Unsupported codec!\n");
        return -1; // Codec not found
    }
    AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
    if (!pCodecContext) {
        printf("failed to allocated memory for AVCodecContext");
        return -1;
    }

    if (avcodec_parameters_to_context(pCodecContext, pCodecParameters) < 0) {
        printf("failed to copy codec params to codec context");
        return -1;
    }

    if (avcodec_open2(pCodecContext, pCodec, nullptr) < 0) {
        printf("failed to open codec through avcodec_open2");
        return -1;
    }

解码视频数据

有了解码器,可以从视频中读取数据包(AVPacket)。AVPacket 为压缩过的数据包,使用函数av_read_frame(pFormatContext, pPacket) 将数据读取到AVPacket中。

AVPacket *pPacket = av_packet_alloc();

// Read frames and save first 500 frames to disk

while (av_read_frame(pFormatContext, pPacket) >= 0) {
    // Is this a packet from the video stream?
    if (pPacket->stream_index == videoStream) {

        // Decode video frame
        int frameFinished = decode_frame(pCodecContext, pFrame, pPacket);

        // Did we get a video frame?
        if (frameFinished) {
          ...
        }
    }

    // Free the packet that was allocated by av_read_frame
    av_packet_unref(pPacket);
}

int decode_frame(AVCodecContext *pCodecContext,
                 AVFrame *pFrame,
                 const AVPacket *pPacket) {
    int frameFinished = 0;
    int ret = avcodec_receive_frame(pCodecContext, pFrame);
    if (ret == 0)
        frameFinished = 1;
    if (ret == 0 || ret == AVERROR(EAGAIN))
        avcodec_send_packet(pCodecContext, pPacket);
    return frameFinished;
}

对于视频,它的帧有多重格式,有I,B,P帧,其中I类型的帧为完整的帧,B类和P类依赖于其它帧(只记录了变化量,这也是压缩的基本思想),因此decode_frame 可能返回不是完整的帧,我们定义一个函数decode_frame,它的返回值frameFinished标明返回的是否是完整的图像帧。

保存彩色图像到硬盘

bunny.mp4图像采用的yuv420p 标准,其中的Y为灰度,你可以参照0_hello_world中直接使用AVFrame保存黑白图像到硬盘。如果需要保存彩色图像,我们需要对图像格式进行转换,也就是从YUV到RGB格式转换。ffmpeg中的swscale 接口可以完成这项工作。

// Allocate an AVFrame structure
AVFrame *pFrameRGB = av_frame_alloc();
if (pFrameRGB == nullptr)
    return -1;

// Determine required buffer size and allocate buffer
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, pCodecContext->width,
                                        pCodecContext->height, IMAGE_ALIGN);
uint8_t *buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));

struct SwsContext *sws_ctx =
        sws_getContext
                (
                        pCodecContext->width,
                        pCodecContext->height,
                        pCodecContext->pix_fmt,
                        pCodecContext->width,
                        pCodecContext->height,
                        AV_PIX_FMT_RGB24,
                        SWS_BILINEAR,
                        nullptr,
                        nullptr,
                        nullptr
                );
        ...

// Convert the image from its native format to RGB
    sws_scale
            (
                    sws_ctx,
                    (uint8_t const *const *) pFrame->data,
                    pFrame->linesize,
                    0,
                    pCodecContext->height,
                    pFrameRGB->data,
                    pFrameRGB->linesize
            );

    ...
    
void save_frame(AVFrame *pFrame,
                int width, int height, int iFrame) {
    FILE *pFile;
    char szFilename[32];
    int y;
    // Open file
    sprintf(szFilename, "frame%d.ppm", iFrame);
    pFile = fopen(szFilename, "wb");
    if (pFile == nullptr)
        return;
    // Write header
    fprintf(pFile, "P6\n%d %d\n255\n", width, height);
    // Write pixel data
    for (y = 0; y < height; y++)
        fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile);
    // Close file
    fclose(pFile);
}

完整代码参见tutorial01.cpp, 运行可以看到bunny.mp4前500帧图像: