【FFmpeg 3.x API应用二】视频编码

摘要

这篇文章介绍使用FFmpeg实现视频解码,具体操作为读取上一节视频解码所生成的YUV420p格式视频文件,对其进行H.264格式视频编码,并将编码后的数据保存为H.264裸流文件Sample.h264。

初始化FFmpeg

所有操作之前必须先注册FFmpeg组件实现全局初始化。

void VideoEncoding::init()
{
    avcodec_register_all();
}

配置编解码器CodecContext

  1. 这里要进行H.264格式的视频编码,所以要先手动指定编码器名字为libx264。更多可用编码器可以使用命令ffmpeg -encoders查看。
  2. 接着调用avcodec_alloc_context3函数申请CodecContext。
  3. 然后手动给CodecContext配置各种编码参数。
  4. 最后调用avcodec_open2完成编码器配置。
// Configure AVCodecContext parameters Manually
bool VideoEncoding::initCodecContext()
{
    const AVCodec *enc = avcodec_find_encoder_by_name("libx264");
    //const AVCodec *enc = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!enc) {
        fprintf(stderr, "Failed to find encoder\n");
        return true;
    }

    mCodecCtx = avcodec_alloc_context3(enc);
    if (!mCodecCtx) {
        printf("Failed to allocate the codec context\n");
        return true;
    }

    // 根据实际情况修改编码器参数
    // 这里只进行编码操作,所以分辨率应和原始文件相同
    mCodecCtx->width = 1280; //与YUV文件分辨率一致。
    mCodecCtx->height = 534;
    mCodecCtx->bit_rate = 1000000; //码率1Mbps
    mCodecCtx->gop_size = 10;
    mCodecCtx->time_base = { 1, 24 };
    mCodecCtx->framerate = { 24, 1 }; //帧率
    mCodecCtx->max_b_frames = 1;
    mCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;  //像素格式YUV420p

    // 根据需要选择编码器预设编码模式
    if (enc->id == AV_CODEC_ID_H264) {
        //此模式编码比较慢,会先缓冲几帧后再进行编码
        //av_opt_set(mCodecCtx->priv_data, "preset", "slow", 0);   // delay ~18 frames

        //这种模式编码速度比较快,没有延时
        av_opt_set(mCodecCtx->priv_data, "tune", "zerolatency", 0);    // no delay
    }

    // Initialize mCodecCtx to use the given Codec
    if (avcodec_open2(mCodecCtx, enc, NULL) < 0) {
        printf("Failed to open codec\n");
        return true;
    }

    return false;
}

读取视频、视频编码、写入文件

  1. 先申请一个buffer用来存放从YUV文件读取到的frame数据。
  2. 与解码写文件操作相逆,按照格式把文件数据循环装到多个frame结构中。我们使用的是YUV420p格式的数据。
  3. 使用avcodec_send_frameavcodec_receive_packet这一对函数进行视频编码,发送一个未编码的frame,接收一个编码后的packet。
  4. 最后把编码后的packet依次写入文件。

需要注意的是,使用不同的编码预设参数会导致编码的速度。也就是说avcodec_send_frame发送一个frame到编码器,可能使用avcodec_receive_packet不能立即接收到一个编码后的packet。所以在发送完所有的frame之后,循环接收编码器返回的编码数据,直接所有的数据接收完毕。最后写入几个字节的H264裸流标识。

bool VideoEncoding::readFrameProc(const char * input, const char * output)
{
    FILE *yuvFd = fopen(input, "rb");   //输入的YUV420p文件Sample.yuv。
    FILE *outFd = fopen(output, "wb");  //输出文件名Sample.h264。
    if (!outFd || !yuvFd) {
        fprintf(stderr, "Could not open file\n");
        return true;
    }

    AVFrame *frame = NULL;
    //申请一个AVFrame
    if (!(frame = av_frame_alloc())) {
        printf("Failed to allocate video frame\n");
        return true;
    }

    //使用CodecContext设置AVFrame参数
    frame->format = mCodecCtx->pix_fmt;    //上一步设置的像素格式 AV_PIX_FMT_YUV420P
    frame->width = mCodecCtx->width;
    frame->height = mCodecCtx->height;

    //为frame申请存储视频的data空间
    if (av_frame_get_buffer(frame, 32) < 0) {
        printf("Failed to allocate the video frame data\n");
        return true;
    }


    int num = 0, i = 0;
    AVPacket pkt;
    // read a frame every time
    while (!feof(yuvFd)) {

        av_init_packet(&pkt);
        pkt.data = NULL;    // packet data will be allocated by the encoder
        pkt.size = 0;

        if (av_frame_make_writable(frame)) {
            return true;
        }

        // 读取yuv420p视频文件到frame.data 
        fread(frame->data[0], 1, mCodecCtx->width *mCodecCtx->height, yuvFd);
        fread(frame->data[1], 1, mCodecCtx->width*mCodecCtx->height / 4, yuvFd);
        fread(frame->data[2], 1, mCodecCtx->width*mCodecCtx->height / 4, yuvFd);

        frame->pts = i++;

        //视频编码:发送frame,接收packet
        avcodec_send_frame(mCodecCtx, frame);
        int ret = avcodec_receive_packet(mCodecCtx, &pkt);

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

    // 编码有延时,获取延时的数据

    for (;; i++) {
        //持续接收编码器发送过来的数据
        avcodec_send_frame(mCodecCtx, NULL);
        int ret = avcodec_receive_packet(mCodecCtx, &pkt);
        if (ret == 0) {
            printf("Write frame %3d (size=%5d)\n", i, pkt.size);
            fwrite(pkt.data, 1, pkt.size, outFd);
            av_packet_unref(&pkt);
        }
        else if (ret == AVERROR_EOF) {    //接收完毕
            printf("Write frame complete\n");
            break;
        }
        else {
            printf("Error encoding frame\n");
            break;
        }

    }

    //看别人都加上这个标识了(#^.^#)
    uint8_t endcode[] = { 0, 0, 1, 0xb7 };
    /* add sequence end code to have a real MPEG file */
    fwrite(endcode, 1, sizeof(endcode), outFd);

    fclose(outFd);
    fclose(yuvFd);
    av_frame_free(&frame);

    return false;
}

释放系统资源

最后要释放相关的资源。

VideoEncoding::~VideoEncoding()
{
    avcodec_free_context(&mCodecCtx);
}

示例程序代码

上述示例的完整代码可以从Github下载: https://github.com/lmshao/FFmpeg-Basic

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章