视觉媒体通信——视频的编解码(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;
 }

h.264视频编解码源代码.rar 详细说明:h.264标准代码,用于视频编码!可以实现各种视频的编码和解码,可以在这个代码的基础上进行各种开发,比如算法的优化,转码技术,实现各种分辨了的转码-h.264 standard code, uses in the video frequency code! May realize each kind of video frequency code and the decoding, may carry on each kind of development in this code foundation, for instance the algorithm optimization, transfers the code technology, realizes each kind has distinguished extension code 文件列表: jm73 ....\JM ....\..\bin ....\..\...\decoder.cfg ....\..\...\encoder.cfg ....\..\...\lencod.exe ....\..\...\lencod.map ....\..\...\lencod.pdb ....\..\CHANGES.TXT ....\..\Changes_detail.txt ....\..\copyright.txt ....\..\disclaimer.txt ....\..\doc ....\..\...\coding_style.doc ....\..\...\doxygen.txt ....\..\...\h26l.css ....\..\...\ldecod.dox ....\..\...\lencod.dox ....\..\encoder.cfg ....\..\foreman_part_qcif.yuv ....\..\ldecod ....\..\......\inc ....\..\......\...\annexb.h ....\..\......\...\biaridecod.h ....\..\......\...\block.h ....\..\......\...\cabac.h ....\..\......\...\context_ini.h ....\..\......\...\contributors.h ....\..\......\...\ctx_tables.h ....\..\......\...\defines.h ....\..\......\...\elements.h ....\..\......\...\erc_api.h ....\..\......\...\erc_do.h ....\..\......\...\erc_globals.h ....\..\......\...\errorconcealment.h ....\..\......\...\fmo.h ....\..\......\...\global.h ....\..\......\...\header.h ....\..\......\...\image.h ....\..\......\...\leaky_bucket.h ....\..\......\...\macroblock.h ....\..\......\...\mbuffer.h ....\..\......\...\mb_access.h ....\..\......\...\memalloc.h ....\..\......\...\nalu.h ....\..\......\...\nalucommon.h ....\..\......\...\output.h ....\..\......\...\parset.h ....\..\......\...\parsetcommon.h ....\..\......\...\rtp.h ....\..\......\...\sei.h ....\..\......\...\vlc.h ....\..\......\Makefile ....\..\......\src ....\..\......\...\annexb.c ....\..\......\...\biaridecod.c ....\..\......\...\block.c ....\..\......\...\cabac.c ....\..\......\...\context_ini.c ....\..\......\...\erc_api.c ....\..\......\...\erc_do_i.c ....\..\......\...\erc_do_p.c ....\..\......\...\errorconcealment.c ....\..\......\...\filehandle.c ....\..\......\...\fmo.c ....\..\......\...\header.c ....\..\......\...\image.c ....\..\......\...\ldecod.c ....\..\......\...\leaky_bucket.c ....\..\......\...\loopFilter.c ....\..\......\...\macroblock.c ....\..\......\...\mbuffer.c ....\..\......\...\mb_access.c ....\..\......\...\memalloc.c ....\..\......\...\nal.c ....\..\......\...\nalu.c ....\..\......\...\nalucommon.c ....\..\......\...\nal_part.c ....\..\......\...\output.c ....\..\......\...\parset.c ....\..\......\...\parsetcommon.c ....\..\......\...\rtp.c ....\..\......\...\sei.c ....\..\......\...\vlc.c ....\..\ldecod.dsp ....\..\ldecod.dsw ... ...
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值