【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

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