目的
使用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),再将包解读为帧。