【西电|视觉媒体通信】H264编解码

目的

使用FFmpeg的编解码功能,将原始视频进行编码,再解码得到重建的原视频。

工具

Visual Studio、FFMPEG、C++。

代码

注意:文件路径要使用"C:\\..."或"C:/",不要使用"C:\",因为"\"在C++中是转义符号。

注意:OpenCV版本为"3.4.16",FFMPEG版本为"gyan.dev -> ffmpeg-6.1.1-full-build-shared.7z"。

注意:OpenCV、FFMPEG需要在Visual Studio中安装。

编码

#define _CRT_SECURE_NO_WARNINGS
// 不加这行 fopen 函数会报错

#include <stdlib.h>
#include <cstdlib>

extern "C"
{
#include <stdio.h>
#include "libavcodec/avcodec.h"
#include "libavutil/imgutils.h"
#include "libavutil/opt.h"
#include "libavformat/avformat.h"
}

static void encode(AVCodecContext* enc_ctx, const AVFrame* frame, AVPacket* pkt, FILE* outfile)
{
    // 对单帧编码
    int ret;
    ret = avcodec_send_frame(enc_ctx, frame);

    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Error sending a frame for encoding\n");
        exit(1);
    }
    while (ret >= 0) {
        ret = avcodec_receive_packet(enc_ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return;
        }
        else if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Error during encoding\n");
            exit(1);
        }

        printf("Write packet %lld (size = %d)\n", pkt->pts, pkt->size);
        fwrite(pkt->data, 1, pkt->size, outfile);

        av_packet_unref(pkt);
    }
}

void executeCmd(const char* cmd)
{
    system(cmd);
}

int main()
{
    int in_width = 832;
    int in_height = 480;
    int in_fps = 30;
    const char* in_file_name = "待编码文件的位置";
    AVPixelFormat in_pix_fmt = AV_PIX_FMT_YUV420P;
    // 待编码文件基本参数

    int out_width = 832;
    int out_height = 480;
    int out_fps = 30;
    const char* out_file_name = "编码得到的H264文件的位置";
    AVCodecID codec_id = AV_CODEC_ID_H264;
    // 编码输出文件基本参数

    FILE* in_file, * out_file;
    in_file = fopen(in_file_name, "rb");
    out_file = fopen(out_file_name, "wb");

    const AVCodec* encodec = avcodec_find_encoder(codec_id);
    if (!encodec) {
        av_log(NULL, AV_LOG_ERROR, "Codec '%s' not found\n", avcodec_get_name(codec_id));
        return 0;
    }
    // 查找并分配编码器

    AVCodecContext* encoder_ctx = avcodec_alloc_context3(encodec);
    if (!encoder_ctx) {
        av_log(NULL, AV_LOG_ERROR, "Could not allocate video codec context\n");
        return 0;
    }
    encoder_ctx->bit_rate = 1000000;
    encoder_ctx->width = out_width;
    encoder_ctx->height = out_height;
    encoder_ctx->framerate = AVRational{ out_fps, 1 };
    encoder_ctx->time_base = AVRational{ 1, out_fps };
    encoder_ctx->gop_size = out_fps;
    encoder_ctx->max_b_frames = 0;
    encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
    // 分配编码器上下文并设置参数

    if (encoder_ctx->codec_id == AV_CODEC_ID_H264) {
        av_opt_set(encoder_ctx->priv_data, "preset", "slow", 0);
    }
    // H264编码器的preset设置为slow,降低编码速度获得高质量视频

    int ret = avcodec_open2(encoder_ctx, encodec, NULL);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Could not open codec\n");
        avcodec_free_context(&encoder_ctx);
        return 0;
    }
    // 打开编码器

    AVFrame* frame = av_frame_alloc();
    frame->width = encoder_ctx->width;
    frame->height = encoder_ctx->height;
    frame->format = encoder_ctx->pix_fmt;
    av_frame_get_buffer(frame, 1);
    AVPacket* pkt = av_packet_alloc();
    // 分配并初始化帧和数据包

    int64_t frame_cnt = 1;
    while (!feof(in_file)) {
        if (fread(frame->data[0], in_width * in_height, 1, in_file) != 1)
            break;
        if (fread(frame->data[1], in_width * in_height / 4, 1, in_file) != 1)
            break;
        if (fread(frame->data[2], in_width * in_height / 4, 1, in_file) != 1)
            break;

        frame->pts = frame_cnt++;

        encode(encoder_ctx, frame, pkt, out_file);
    }
    // 逐帧读取并编码视频

    encode(encoder_ctx, NULL, pkt, out_file);
    // 最后一帧可能不是完整的帧,本句保证YUV所有帧被编码

    fclose(in_file);
    fclose(out_file);
    avcodec_free_context(&encoder_ctx);
    av_frame_free(&frame);
    av_packet_free(&pkt);
    // 结束编码,释放资源

    return 0;
}

解码

#define _CRT_SECURE_NO_WARNINGS
// 不加这行 fopen 函数会报错

#include <iostream>

extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/pixdesc.h"
#include "libavutil/frame.h"
#include "libavutil/imgutils.h"
}

int main()
{
    const char* input_file = "待解码的H264文件的位置";
    int ret;
    AVFormatContext* input_fmt_ctx = NULL;

    if ((ret = avformat_open_input(&input_fmt_ctx, input_file, NULL, NULL)) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
        return ret;
    }
    if ((ret = avformat_find_stream_info(input_fmt_ctx, NULL)) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
        return ret;
    }
    av_dump_format(input_fmt_ctx, 0, input_file, 0);
    // 打开文件,获取流信息

    int video_stream_index = -1;
    const AVCodec* video_codec = avcodec_find_decoder(AV_CODEC_ID_H264);
    if ((ret = av_find_best_stream(input_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &video_codec, -1)) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot find an video stream in the input file\n");
        avformat_close_input(&input_fmt_ctx);
        return ret;
    }
    video_stream_index = ret;
    // 查找最优视频流,获取对应解码器

    AVCodecParameters* codecpar = input_fmt_ctx->streams[video_stream_index]->codecpar;
    if (!video_codec)
    {
        av_log(NULL, AV_LOG_ERROR, "Can't find decoder\n");
        return -1;
    }
    AVCodecContext* video_decoder_ctx = avcodec_alloc_context3(video_codec);
    if (!video_decoder_ctx)
    {
        av_log(NULL, AV_LOG_ERROR, "Could not allocate a decoding context\n");
        avformat_close_input(&input_fmt_ctx);
        return AVERROR(ENOMEM);
    }
    if ((ret = avcodec_parameters_to_context(video_decoder_ctx, codecpar)) < 0)
    {
        avformat_close_input(&input_fmt_ctx);
        avcodec_free_context(&video_decoder_ctx);
        return ret;
    }
    if ((ret = avcodec_open2(video_decoder_ctx, video_codec, NULL)) < 0)
    {
        avformat_close_input(&input_fmt_ctx);
        avcodec_free_context(&video_decoder_ctx);
        return ret;
    }
    // 初始化解码器上下文

    uint32_t frameCounter = 0;
    AVPacket* pkt = av_packet_alloc(); // 解码前的数据包
    AVFrame* frame = av_frame_alloc(); // 解码后的帧数据
    FILE* fyuv = fopen("解码得到的YUV文件的位置", "wb");
    AVFrame* frame_yuv = av_frame_alloc();
    frame_yuv->width = video_decoder_ctx->width;
    frame_yuv->height = video_decoder_ctx->height;
    av_image_alloc(frame_yuv->data, frame_yuv->linesize,
        frame_yuv->width, frame_yuv->height, AV_PIX_FMT_YUV420P, 1);
    SwsContext* sws_ctx =
        sws_getContext(video_decoder_ctx->width, video_decoder_ctx->height, video_decoder_ctx->pix_fmt,
            frame_yuv->width, frame_yuv->height, AV_PIX_FMT_YUV420P,
            SWS_BILINEAR, NULL, NULL, NULL);
    // 分配AVPacket和AVFrame,使用双线性插值算法,输出YUV文件

    while (av_read_frame(input_fmt_ctx, pkt) >= 0)
    {
        if (pkt->stream_index != video_stream_index)
            continue;

        ret = avcodec_send_packet(video_decoder_ctx, pkt);

        while (ret >= 0)
        {
            ret = avcodec_receive_frame(video_decoder_ctx, frame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            {
                break;
                // 如果解码器需要更多数据或到达文件末尾,跳出循环
            }
            else if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "Error while sending a packet to the decoder\n");
                goto end;
            }

            sws_scale(sws_ctx,
                frame->data, frame->linesize, 0, frame->height,
                frame_yuv->data, frame_yuv->linesize);
            // 将解码后的帧转换为YUV-I420格式
            fwrite(frame_yuv->data[0], 1, frame_yuv->width * frame_yuv->height * 3 / 2, fyuv);
            printf("\rSucceed to decode frame %d\n", frameCounter++);
            av_frame_unref(frame);
        }
        av_packet_unref(pkt);
    }
    // 循环读取视频帧并解码

    while (1)
    {

        ret = avcodec_send_packet(video_decoder_ctx, NULL);
        if (ret < 0)
            break;

        while (ret >= 0)
        {
            ret = avcodec_receive_frame(video_decoder_ctx, frame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            {
                break;
            }
            else if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "Error while sending a packet to the decoder\n");
                goto end;
            }

            sws_scale(sws_ctx,
                frame->data, frame->linesize, 0, frame->height,
                frame_yuv->data, frame_yuv->linesize);

            fwrite(frame_yuv->data[0], 1, frame_yuv->width * frame_yuv->height * 3 / 2, fyuv);
            printf("\rSucceed to decode frame %d\n", frameCounter++);

            av_frame_unref(frame);
        }
    }
    // 循环结束后,处理可能剩余的帧数据,比如B帧

end:
    avcodec_free_context(&video_decoder_ctx);
    avformat_close_input(&input_fmt_ctx);
    av_packet_free(&pkt);
    av_frame_free(&frame);
    fclose(fyuv);
    // 释放资源,关闭文件

    return 0;
}

原理

编码和解码的过程是将帧(Frame)写成包(Packet),再将包解读为帧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值