ffmpeg工作流程分析-3

为掌握ffmpeg的工作流程,现以libavcodec下的ffmpeg/doc/examples/decoding_encoding.c中实现视频编码为例展示其工作过程。

libavcodec库是实现音频或视频的编码或解码,所有编解码CODEC的调用均有统一的格式,ffmpeg以公共的函数指针注册、调用和销毁
CODEC。

video_encode_example例程实现给定视频数据的编码,将编码后的码流写文件。所有avcodec的调用均使用统一的过程,首先是初始化avcodec_init(),所有CODEC库在使用前均需要调用该函数;然后是注册所有的CODEC,包括注册编码器REGISTER_ENCODER、解码器REGISTER_DECODER、编解码器REGISTER_ENCDEC等;最后使用CODEC,如video_encode_example。

1 初始化CODEC

调用ffmpeg的函数avcodec_init实现CODEC的初始化。

static av_cold void avcodec_init(void)
{
    static int initialized = 0; //静态变量标注初始化,只调用一次

    if (initialized != 0)
        return;
    initialized = 1;

    if (CONFIG_ME_CMP)
        ff_me_cmp_init_static();
}

为确保该初始化仅被调用一次,因此使用局部静态变量,记录以前的动作。初始化的任务是将“数字信号处理应用”中的静态表格初始化。其实就是一个数组,初始化其为0。
初始化的函数会在这里被调用:

av_cold void avcodec_register(AVCodec *codec)
{
    AVCodec **p;
    avcodec_init();
    p = last_avcodec;
    codec->next = NULL;

    while(*p || avpriv_atomic_ptr_cas((void * volatile *)p, NULL, codec))
        p = &(*p)->next;
    last_avcodec = &codec->next;

    if (codec->init_static_data)
        codec->init_static_data(codec);
}

2 注册CODEC

由于有诸多的CODEC,所以ffmpeg以公共的函数指针的方式,以统一的接口操作CODEC。首先AVCodec结构体定义了函数指针,某一个CODEC实例填充AVCodec结构的字段,然后用REGISTER_ENCODER注册该CODEC。


struct AVSubtitle;

/** * AVCodec. */
typedef struct AVCodec {
    /** * Name of the codec implementation. * The name is globally unique among encoders and among decoders (but an * encoder and a decoder can share the same name). * This is the primary way to find a codec from the user perspective. */
    const char *name;
    /** * Descriptive name for the codec, meant to be more human readable than name. * You should use the NULL_IF_CONFIG_SMALL() macro to define it. */
    const char *long_name;
    enum AVMediaType type;
    enum AVCodecID id;
    /** * Codec capabilities. * see AV_CODEC_CAP_* */
    int capabilities;
    const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0}
    const enum AVPixelFormat *pix_fmts;     ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1
    const int *supported_samplerates;       ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0
    const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1
    const uint64_t *channel_layouts;         ///< array of support channel layouts, or NULL if unknown. array is terminated by 0
    uint8_t max_lowres;                     ///< maximum value for lowres supported by the decoder, no direct access, use av_codec_get_max_lowres()
    const AVClass *priv_class;              ///< AVClass for the private context
    const AVProfile *profiles;              ///< array of recognized profiles, or NULL if unknown, array is terminated by {FF_PROFILE_UNKNOWN}

    /***************************************************************** * No fields below this line are part of the public API. They * may not be used outside of libavcodec and can be changed and * removed at will. * New public fields should be added right above. ***************************************************************** */
    int priv_data_size;
    struct AVCodec *next;
    /** * @name Frame-level threading support functions * @{ */
    /** * If defined, called on thread contexts when they are created. * If the codec allocates writable tables in init(), re-allocate them here. * priv_data will be set to a copy of the original. */
    int (*init_thread_copy)(AVCodecContext *);
    /** * Copy necessary context variables from a previous thread context to the current one. * If not defined, the next thread will start automatically; otherwise, the codec * must call ff_thread_finish_setup(). * * dst and src will (rarely) point to the same context, in which case memcpy should be skipped. */
    int (*update_thread_context)(AVCodecContext *dst, const AVCodecContext *src);
    /** @} */

    /** * Private codec-specific defaults. */
    const AVCodecDefault *defaults;

    /** * Initialize codec static data, called from avcodec_register(). */
    void (*init_static_data)(struct AVCodec *codec);

    int (*init)(AVCodecContext *);
    int (*encode_sub)(AVCodecContext *, uint8_t *buf, int buf_size,
                      const struct AVSubtitle *sub);
    /** * Encode data to an AVPacket. * * @param avctx codec context * @param avpkt output AVPacket (may contain a user-provided buffer) * @param[in] frame AVFrame containing the raw data to be encoded * @param[out] got_packet_ptr encoder sets to 0 or 1 to indicate that a * non-empty packet was returned in avpkt. * @return 0 on success, negative error code on failure */
    int (*encode2)(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame,
                   int *got_packet_ptr);
    int (*decode)(AVCodecContext *, void *outdata, int *outdata_size, AVPacket *avpkt);
    int (*close)(AVCodecContext *);
    /** * Flush buffers. * Will be called when seeking */
    void (*flush)(AVCodecContext *);
    /** * Internal codec capabilities. * See FF_CODEC_CAP_* in internal.h */
    int caps_internal;
} AVCodec;

上述的AVCodec结构体定义了关于CODEC的一些标识信息、操作的函数指针,该结构是所有CODEC均具有的属性。CODEC的操作通过init、encode、close或decode函数指针实现某个CODEC的处理动作。下面就是一个CODEC结构体的初始化。

AVCodec ff_mpeg1video_encoder = {
    .name                 = "mpeg1video",
    .long_name            = NULL_IF_CONFIG_SMALL("MPEG-1 video"),
    .type                 = AVMEDIA_TYPE_VIDEO,
    .id                   = AV_CODEC_ID_MPEG1VIDEO,
    .priv_data_size       = sizeof(MpegEncContext),
    .init                 = encode_init,
    .encode2              = ff_mpv_encode_picture,
    .close                = ff_mpv_encode_end,
    .supported_framerates = ff_mpeg12_frame_rate_tab + 1,
    .pix_fmts             = (const enum AVPixelFormat[]) { AV_PIX_FMT_YUV420P,
                                                           AV_PIX_FMT_NONE },
    .capabilities         = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_SLICE_THREADS,
    .priv_class           = &mpeg1_class,
};

上述初始化中的三个函数encode_init、MPV_encode_picture、MPV_encode_end实现具体的数据处理,即初始化CODEC,编码、关闭CODEC。定义CODEC之后,就需要注册该CODEC。即调用函数avcodec_register_all实现注册。

void avcodec_register_all(void)
{
    static int initialized;

    if (initialized)
        return;
    initialized = 1;

    /* hardware accelerators */
    REGISTER_HWACCEL(H263_VAAPI,        h263_vaapi);
    REGISTER_HWACCEL(H263_VIDEOTOOLBOX, h263_videotoolbox);
    REGISTER_HWACCEL(H264_D3D11VA,      h264_d3d11va);
    REGISTER_HWACCEL(H264_DXVA2,        h264_dxva2);
    REGISTER_HWACCEL(H264_MMAL,         h264_mmal);
    REGISTER_HWACCEL(H264_QSV,          h264_qsv);
    REGISTER_HWACCEL(H264_VAAPI,        h264_vaapi);
    REGISTER_HWACCEL(H264_VDA,          h264_vda);
    REGISTER_HWACCEL(H264_VDA_OLD,      h264_vda_old);
    REGISTER_HWACCEL(H264_VDPAU,        h264_vdpau);
    REGISTER_HWACCEL(H264_VIDEOTOOLBOX, h264_videotoolbox);
    REGISTER_HWACCEL(HEVC_D3D11VA,      hevc_d3d11va);
    REGISTER_HWACCEL(HEVC_DXVA2,        hevc_dxva2);
    REGISTER_HWACCEL(HEVC_QSV,          hevc_qsv);
    REGISTER_HWACCEL(HEVC_VAAPI,        hevc_vaapi);
    REGISTER_HWACCEL(HEVC_VDPAU,        hevc_vdpau);
    REGISTER_HWACCEL(MPEG1_XVMC,        mpeg1_xvmc);
    REGISTER_HWACCEL(MPEG1_VDPAU,       mpeg1_vdpau);
    REGISTER_HWACCEL(MPEG1_VIDEOTOOLBOX, mpeg1_videotoolbox);
    REGISTER_HWACCEL(MPEG2_XVMC,        mpeg2_xvmc);
    ...

上述的宏REGISTER_ENCDEC进一步使用子宏REGISTER_ENCODER、
REGISTER_DECODER,并实际调用avcodec_register()函数实现CODEC的注册。

av_cold void avcodec_register(AVCodec *codec)
{
    AVCodec **p;
    avcodec_init();
    p = last_avcodec;
    codec->next = NULL;

    while(*p || avpriv_atomic_ptr_cas((void * volatile *)p, NULL, codec))
        p = &(*p)->next;
    last_avcodec = &codec->next;

    if (codec->init_static_data)
        codec->init_static_data(codec);
}

上述过程实现所有的CODEC通过next指针而连接起来,形成一个链表每个节点为一个CODEC。

3 使用CODEC

完成注册后,接下来就是使用CODEC了。下面以实现YUV420格式的Mpeg-1视频编码video_encode_example为例,说明使用CODEC的过程。

/* * Video encoding example */
static void video_encode_example(const char *filename, int codec_id) //参数为待输出的码流文件和codec的id
{
    AVCodec *codec; //编码器
    AVCodecContext *c= NULL; //编码器上下文
    int i, ret, x, y, got_output;
    FILE *f;
    AVFrame *frame;
    AVPacket pkt;
    uint8_t endcode[] = { 0, 0, 1, 0xb7 };

    printf("Encode video file %s\n", filename);

    /* find the mpeg1 video encoder */
    codec = avcodec_find_encoder(codec_id);// 查找mpeg1视频编码器
    if (!codec) {
        fprintf(stderr, "Codec not found\n");
        exit(1);
    }

    c = avcodec_alloc_context3(codec); // 分配codec结构
    if (!c) {
        fprintf(stderr, "Could not allocate video codec context\n");
        exit(1);
    }

    /* put sample parameters */
    c->bit_rate = 400000;
    /* resolution must be a multiple of two */
    c->width = 352;
    c->height = 288;
    /* frames per second */
    c->time_base = (AVRational){1,25};
    /* emit one intra frame every ten frames * check frame pict_type before passing frame * to encoder, if frame->pict_type is AV_PICTURE_TYPE_I * then gop_size is ignored and the output of encoder * will always be I frame irrespective to gop_size */
    c->gop_size = 10;
    c->max_b_frames = 1;
    c->pix_fmt = AV_PIX_FMT_YUV420P;

    if (codec_id == AV_CODEC_ID_H264)
        av_opt_set(c->priv_data, "preset", "slow", 0);

    /* open it */ //打开codec
    if (avcodec_open2(c, codec, NULL) < 0) {
        fprintf(stderr, "Could not open codec\n");
        exit(1);
    }

    f = fopen(filename, "wb");
    if (!f) {
        fprintf(stderr, "Could not open %s\n", filename);
        exit(1);
    }

    frame = av_frame_alloc();
    if (!frame) {
        fprintf(stderr, "Could not allocate video frame\n");
        exit(1);
    }
    frame->format = c->pix_fmt;
    frame->width  = c->width;
    frame->height = c->height;

    /* the image can be allocated by any means and av_image_alloc() is * just the most convenient way if av_malloc() is to be used */
    ret = av_image_alloc(frame->data, frame->linesize, c->width, c->height,
                         c->pix_fmt, 32);
    if (ret < 0) {
        fprintf(stderr, "Could not allocate raw picture buffer\n");
        exit(1);
    }

    /* encode 1 second of video */
    for (i = 0; i < 25; i++) {
        av_init_packet(&pkt);
        pkt.data = NULL;    // packet data will be allocated by the encoder
        pkt.size = 0;

        fflush(stdout);
        /* prepare a dummy image */
        /* Y */
        for (y = 0; y < c->height; y++) {
            for (x = 0; x < c->width; x++) {
                frame->data[0][y * frame->linesize[0] + x] = x + y + i * 3;
            }
        }

        /* Cb and Cr */
        for (y = 0; y < c->height/2; y++) {
            for (x = 0; x < c->width/2; x++) {
                frame->data[1][y * frame->linesize[1] + x] = 128 + y + i * 2;
                frame->data[2][y * frame->linesize[2] + x] = 64 + x + i * 5;
            }
        }

        frame->pts = i;

        /* encode the image */
        ret = avcodec_encode_video2(c, &pkt, frame, &got_output);
        if (ret < 0) {
            fprintf(stderr, "Error encoding frame\n");
            exit(1);
        }

        if (got_output) {
            printf("Write frame %3d (size=%5d)\n", i, pkt.size);
            fwrite(pkt.data, 1, pkt.size, f);
            av_packet_unref(&pkt);
        }
    }

    /* get the delayed frames */ // 获得延迟的帧,如B帧编码方式
    for (got_output = 1; got_output; i++) {
        fflush(stdout);

        ret = avcodec_encode_video2(c, &pkt, NULL, &got_output);
        if (ret < 0) {
            fprintf(stderr, "Error encoding frame\n");
            exit(1);
        }

        if (got_output) {
            printf("Write frame %3d (size=%5d)\n", i, pkt.size);
            fwrite(pkt.data, 1, pkt.size, f);
            av_packet_unref(&pkt);
        }
    }

    /* add sequence end code to have a real mpeg file */
    fwrite(endcode, 1, sizeof(endcode), f); //写入结束符
    fclose(f); // 关闭码流文件

    avcodec_close(c); // 关闭解码器
    av_free(c); // 释放解码器结构
    av_freep(&frame->data[0]); //?
    av_frame_free(&frame); //释放帧结构体
    printf("\n");
}

上述代码展示了使用CODEC的过程,首先用函数avcodec_find_encoder()查找编码器;然后用函数avcodec_alloc_context()申请CODEC,函数avcodec_alloc_frame()申请编码器中的图像帧空间;设置编码器参数,包括宽度、高度等;avcodec_open()打开编码器CODEC;获取图像数据;编码当前图像avcodec_encode_video();写入码流文件;编码完毕后,销毁各种资源,关闭编码器avcodec_close()等。其他CODEC的使用方法基本与上述的MPEG-1视频编码器的工作过程相同,区别主要在于CODEC的工作参数的设置。

你可能感兴趣的:(视频,编码,ffmpeg)