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。

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