视觉媒体通信——视频的编解码(H.264)

0 引言

        现如今的主流视频编解码方式有多种,其中最为常见和广泛应用的是H.264、H.265(也称为HEVC)、AV1等。视频编解码技术对于数字视频的传输、存储和播放起着关键的作用。它们能够压缩视频数据,减少传输带宽和存储空间的需求,并且保持高质量的视频观看体验。

        H.264,也被称为Advanced Video Coding (AVC),是一种广泛应用的视频编解码标准。它是由国际电信联盟(ITU)和国际标准化组织(ISO)共同制定的,并于2003年发布。H.264具有高压缩性能和广泛的应用支持,成为当今主流的视频编解码标准之一。

        本篇将实现利用现有编码器ffmpeg实现视频h.264编解码。本次用到的版本为Visual Studio 2019,ffmpeg6.0

1 实验目标

        编程实现视频的编解码应用:使用现有编解码器(ffmpeg,编码标准H.264),将原始视频进行编码、解码得到重建视频,分析压缩码率与视频质量的关系。

2 实验内容

2.1 实现框图

         H.264视频编码仅支持对YUV数据进行编码,所以我们应当输入yuv视频流进行相应的编解码。

2.2 H.264编码

        H.264编码器的主要算法流程如下图所示:

(1)输入YUV文件。包括输入视频的分辨率帧率。

(2)调用avcodec_find_encoder 根据视频流信息的codec_id找到对应的解码器(H.264)。

(3)编码器初始化。调用avcodec_alloc_contex3分配解码器内存。

(4)编码参数设置。包括编码后的分辨率、帧率、编码速率、输出格式(YUV)、B帧率等。

(5)打开解码器。调用函数avcodec_open2 使用给定的AVCodec初始化AVCodecContext。

(6)分配变量空间。

(7)转换处理。将YUV三通道数据进行转换处理。

(8)调用avcodec_send_packet送一帧到编码器,avcodec_receive_frame尝试获取编码数据。

(9)将编好的数据(.h264)写入文件。

(10)编码完成,释放资源。

2.3 H.264解码

        H.264解码器的主要算法流程如下图所示:

(1)调用avformat_open_input来打开媒体文件,该文件是用编码函数所编码的H.264文件。

(2)调用avformat_find_stream_info 初始化AVFormatContext。

(3)匹配到视频流的索引index

(4)调用avcodec_find_decoder 根据视频流信息的codec_id找到对应的解码器(H.264)。

(5)解码器初始化。调用avcodec_alloc_contex3分配解码器内存。

(6)解码器参数设置。

(7)打开解码器。调用函数avcodec_open2 使用给定的AVCodec初始化AVCodecContext。

(8)初始化输出文件、解码AVPacket和AVFrame结构体,用于管理缓存区。

(9)调用av_read_frame循环从输入中读取一帧压缩帧。

(10)调用avcodec_send_packet送一帧到解码器,avcodec_receive_frame尝试获取解码数据。

(11)调用sws_scale进行格式转换,写入YUV文件。

(12)FLUSH解码器。由于压缩编码数据在解码器中存在缓冲或者延时,一些packet并不由decoder直接解码输出,需要decoding结束时flush,从而获得所有的解码数据。

(13)解码完成,释放资源。

2.4 解码视频播放

        经解码后得到的视频格式为.yuv格式,因此可以用第一次实验中的YUV播放器进行观看。

3 实验结果

3.1 原始视频

        我处理的仍然是第一次用到的yuv视频。

         原始视频分辨率为176×144,帧率为25FPS,共有870帧。

3.2 编码处理结果

         可以看到,总共成功编码870帧(1-870),码率约为1Mbps。接着我继续查看编码后的视频文件与源文件。

编码前(左)后(右)文件大小对比

         可以看到,编码前YUV文件大小为31.5MB,在码率为1M且不改变分辨率和帧率情况下,经过H.264编码后,文件大小变为4.15MB。

3.3 解码处理结果

         可以看到,总共成功编码870帧(0-869),视频的分辨率为176×144,帧率为25FPS。经过解码后,恢复的YUV文件大小为31.5MB,与源文件一致。

3.4 解码处理后的视频

      将解码处理后的yuv视频进行播放。左图为原视频文件,右图为经过编解码处理后的视频。

        编解码处理后的视频分辨率为176×144,帧率为25FPS,共有870帧。色彩正常,视频清晰度较高。

3.5 改变编码速率

         可以看到,总共成功编码870帧(1-870),编码速率约为64kbps。

编码前(左)后(右)文件大小对比

        可以看到,编码前YUV文件大小为31.5MB,在编码速率为64k且不改变分辨率和帧率情况下,经过H.264编码后,文件大小变为277KB,大大减小。

         可以看到,总共成功编码870帧(0-869),视频的分辨率为176×144,帧率为25FPS。经过解码后,恢复的YUV文件大小为31.5MB,与源文件一致。编解码处理前(左)后(右)如下图所示:

        在码率为64kbps,编解码处理后的视频相比于原始视频色彩正常,视频清晰度略有降低。由于该视频本身分辨率不够高,我再次降低编码速率,以此来比较视频质量。将编码速率调整为32kbps、16kbps。

原始(左上)、码率64kbps(右上)、码率32kbps(左下)、码率16kbps(右下)

         可以看到,在相同的视频编码方式下,随着压缩码率的降低,由此视频的质量越来越差(非线性)。但是与此同时编码后的文件大小也降低。因此,权衡好清晰度和文件大小,来选择最佳的压缩码率。

4 完整代码

4.1 H.264编码部分

#include <stdio.h>
#include "libavcodec/avcodec.h"
#include "libavutil/imgutils.h"
#include "libavutil/opt.h"

#ifdef __cplusplus  
extern "C" {
#endif  

#ifdef __cplusplus  
}
#endif 

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);
        }

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

        av_packet_unref(pkt);
    }
}

#define INPUT_FORMAT_YUV420P

int main()
{
    // 输入视频文件信息
    int in_width = 176;
    int in_height = 144;
    int in_fps = 25;

#ifdef INPUT_FORMAT_YUV420P
    const char* in_file_name = "D:\\grandma_qcif.yuv";
    AVPixelFormat in_pix_fmt = AV_PIX_FMT_YUV420P;
#endif

    // 输出编码流文件信息(和输入相同)
    int out_width = 176;
    int out_height = 144;
    int out_fps = 25;

    const char* out_file_name = "grandma_qcif.h264";
    AVCodecID codec_id = AV_CODEC_ID_H264;

    // 输入输出文件
    FILE* in_file, * out_file;
    in_file = fopen(in_file_name, "rb");
    if (!in_file) {
        fprintf(stderr, "Can not open file %s\n", in_file_name);
        return 0;
    }
    out_file = fopen(out_file_name, "wb");
    if (!out_file) {
        fprintf(stderr, "Can not open file %s\n", out_file_name);
        return 0;
    }

    // video encoder
    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;
    }

    // video encoder contex
    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 parameters
    encoder_ctx->bit_rate = 1000000; // 1Mbps
    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;   // B帧数
    encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P;  // H264不支持RGB

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

    // open condec
    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;
    }

    // allocate variables
    // 编码器实际输入(参数和编码器保持一致)
    AVFrame* frame = av_frame_alloc();
    // 供解码器使用,需要设定width、height、format,否则会出现警告
    frame->width = encoder_ctx->width;
    frame->height = encoder_ctx->height;
    frame->format = encoder_ctx->pix_fmt;

    // Y,U,V三个通道内存不连续,1字节对齐
    av_frame_get_buffer(frame, 1); 
    AVPacket* pkt = av_packet_alloc();

    // start encoder
    int64_t frame_cnt = 1;
    while (!feof(in_file)) {
        // Y,U,V三个通道内存不连续,单通道内存1字节对齐连续
        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);
    }

    // flush
    encode(encoder_ctx, NULL, pkt, out_file);

    fclose(in_file);
    fclose(out_file);

    avcodec_free_context(&encoder_ctx);
    av_frame_free(&frame);
    av_packet_free(&pkt);
}

4.2 H.264解码

#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 = "D:\\grandma_qcif.h264";

    int ret;
    AVFormatContext* input_fmt_ctx = NULL; // 必须设置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;

    // const AVCodec* video_codec = avcodec_find_decoder(AV_CODEC_ID_H264);
    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 frameCnt = 0;
    AVPacket* pkt = av_packet_alloc(); // 分配一个AVPactet对象,用于管理其缓冲区
    AVFrame* frame = av_frame_alloc(); // 分配一个AVFrame对象,用于管理其缓冲区

    FILE* fyuv = fopen("out.yuv", "wb");

    // yuv420p对齐处理 变量
    AVFrame* frame_yuv = av_frame_alloc();
    // 分配缓冲区,接收转换后yuv420p的1字节对齐数据,分辨率不改变
    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_scale调用
    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);                                                 // 变换处理

    while (av_read_frame(input_fmt_ctx, pkt) >= 0) { // 循环从输入获取一帧压缩编码数据,分配pkt缓冲区  

        // 仅处理视频码流
        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); // 尝试获取解码数据,分配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", frameCnt++);
                av_frame_unref(frame);  // 释放frame缓冲区数据
            }
            av_packet_unref(pkt); // 释放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", frameCnt++);

                av_frame_unref(frame);  // 释放frame缓冲区数据
            }
        }
    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;
 }

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值