一、yuv编码为h264
使用yuv编码为h264,然后用ffplay播放h264文件
先准备一个mp4文件 然后使用ffmpeg命令提取视频数据并转为yuv格式
ffmpeg -i input.mp4 -pixel_format yuv420p -s 1280x720 output.yuv
把 input.mp4 里的视频“解码 → 颜色转换 → 缩放”后,按 720p YUV420p 的裸像素流输出到 output.yuv,注意提取的是纯YUV视频数据,没有音频数据,后面编码后播放也只是视频数据
最后编码成功的h264可以进行ffplay播放
ffplay -i input.264
编码流程:
yuv转h264函数体
整体流程:
pro.工程文件链接头文件和库文件
DEFINES += PROJECT_ROOT=\\\"$$PWD\\\" #.pro文件所在目录
# 当前目录下的 FFMPEG_Packet
FFMPEG_DIR = $$PWD/ffmpeg_packet
INCLUDEPATH += $$FFMPEG_DIR/include
LIBS += -L$$FFMPEG_DIR/lib \
-lavcodec \
-lavformat \
-lavutil \
-lswresample \
-lswscale
yuv2h264函数
int encoder::yuv2h264()
{
QString projectRoot = QStringLiteral(PROJECT_ROOT); //.pro文件所在目录
qDebug() << projectRoot;
QString inFilePath = projectRoot + "/output.yuv";
QString outFilePath = projectRoot + + "/output.h264";
int ret = 0;
//准备编码器
const AVCodec* codec = nullptr;
AVCodecContext* codecContent = nullptr; //上下文
AVPacket* packet = nullptr;
AVFrame* frame = nullptr;
FILE* inFile = nullptr, *outFile = nullptr;
//查找264编码器
codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
qDebug() << "avcodec_find_decoder failed";
return -1;
}
//分配编码器上下文
codecContent = avcodec_alloc_context3(codec);
if (!codecContent) { qDebug() << "avcodec_alloc_context3 failed";
return -2;
}
//设置上下文参数
codecContent->width = 1280;
codecContent->height = 720;
codecContent->time_base = AVRational{1,25};
codecContent->pix_fmt = AV_PIX_FMT_YUV420P;
codecContent->framerate = AVRational{25,1};
//打开编码器
ret = avcodec_open2(codecContent, codec, NULL);
if (ret != 0){
qDebug() << "avcodec_open2 failed";
return -3;
}
//分配编码包
packet = av_packet_alloc();
if (!packet) return -4;
//分配编码帧
frame = av_frame_alloc();
if (!frame){
return -5;
}
//设置帧参数
frame->width = 1280;
frame->height = 720;
frame->format = AV_PIX_FMT_YUV420P;
//申请frame的内存来存放帧数据
ret = av_frame_get_buffer(frame, 0);
if (ret){
qDebug() << "av_frame_get_buffer failed";
return -6;
}
//打开输入文件读数据
inFile = fopen(inFilePath.toStdString().c_str(), "rb");
if (!inFile){
qDebug() << "open inFile failed";
return -7;
}
//打开输出文件
outFile = fopen(outFilePath.toStdString().c_str(), "wb");
if (!outFile){
qDebug() << "open outFile failed";
return -8;
}
int frame_count = 0;
//循环读数据编码
while (!feof(inFile)) {
ret = av_frame_is_writable(frame); //检查frame是否可写
if (ret < 0){
//我们要设置为可写
ret = av_frame_make_writable(frame);
}
// 从yuv文件读取y u v分量到frame中 然后编码
fread(frame->data[0], 1, frame->width * frame->height, inFile); //y分量
fread(frame->data[1], 1, frame->width * frame->height / 4, inFile); //u分量
fread(frame->data[2], 1, frame->width * frame->height / 4, inFile); //v分量
frame->pts = frame_count++; //设置帧的时间戳
//真正地编码
encode(codecContent, packet, frame, outFile);
}
//编码完成后 发送空的帧 告诉编码器 已经没有数据了 刷新编码器
encode(codecContent, packet, NULL, outFile);
qDebug() << "encode success";
//释放资源
fclose(inFile);
fclose(outFile);
av_frame_free(&frame);
av_packet_free(&packet);
avcodec_free_context(&codecContent);
return 0;
}
encode函数体
int encoder::encode(AVCodecContext *codecContext, AVPacket *packet, AVFrame *frame, FILE *outFile)
{
int ret = avcodec_send_frame(codecContext, frame);
if (ret < 0){
qDebug() << "avcodec_send_frame failed";
return -1;
}
// 编码成功开始循环接收
while (ret == 0) {
ret = avcodec_receive_packet(codecContext, packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
return 0;
}
else if(ret < 0){ //接收失败
return -1;
}
else if (ret == 0){
//接收成功 写入到输出文件
fwrite(packet->data, 1, packet->size, outFile);
}
}
return 0;
}
编码成功
从libx264编码器输出日志中可以看到一些信息:
YUV 4:2:0色度抽样,8位深度,目标码率:2032.23 kb/s,I帧(关键帧):5个,平均QP(量化参数)19.39,平均大小27,843字节,P帧(预测帧):254个,平均QP 22.04,平均大小11,583字节,B帧(双向预测帧):101个,平均QP 23.26,平均大小5,711字节,B帧连续比例:56.1%连续B帧,16.7%非连续
最后用ffplay播放即可
二、pcm编码为aac
使用pcm编码为aac,然后使用ffplay播放
先准备一个mp4文件 然后使用ffmpeg命令提取音频数据并转为pcm格式,设置音频采样率为44.1kHz,设置为双声道立体声,指定输出格式为有符号16位小端PCM
ffmpeg -i input.mp4 -ar 44100 -ac 2 -f s16le output.pcm
最后编码成功的aac可以进行ffplay播放
ffplay -i output.aac
pcm转aac函数整体流程
pcm2aac函数体
int encoder::pcm2aac()
{
QString projectRoot = QStringLiteral(PROJECT_ROOT); //.pro文件所在目录
qDebug() << projectRoot;
QString inputfile = projectRoot + "/output.pcm";
QString outputfile = projectRoot + "/output.aac";
//1. 查找编码器
const AVCodec* avcodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
if (!avcodec){
qDebug() << "avcodec_find_encoder failed";
return -1;
}
//2. 设置编码器
//分配编码器上下文
AVCodecContext* avcodecContext = avcodec_alloc_context3(avcodec);
if (!avcodecContext){
qDebug() << "avcodec_alloc_context3 failed";
return -2;
}
//设置上下文参数
avcodecContext->sample_rate = 44100; //采样率
avcodecContext->channels = 2; //通道数
avcodecContext->sample_fmt = AV_SAMPLE_FMT_FLTP; //样本格式
avcodecContext->channel_layout = AV_CH_LAYOUT_STEREO; //声道布局 //立体声
avcodecContext->bit_rate = 64000; //比特率
//3. 打开解码器
int ret = 0;
ret = avcodec_open2(avcodecContext, avcodec, NULL);
if (ret != 0){ qDebug() << "avcodec_open2 failed";
return -3;
}
//4. 分配创建输出上下文
AVFormatContext* avFormatContext = nullptr;
avformat_alloc_output_context2(&avFormatContext, NULL, NULL, outputfile.toStdString().c_str());
if (!avFormatContext){
qDebug() << "avformat_alloc_output_context2 failed";
return -4;
}
//5. 创建流并添加到 AVFormatContext 输出上下文中 一个 AVFormatContext 可以包含多个流,例如视频流和音频流
AVStream* st = avformat_new_stream(avFormatContext, NULL);
if (!st){
qDebug() << "avformat_new_stream failed";
return -5;
}
//6. 设置流参数 将编码器上下文的参数复制到流的参数 保了流的参数与编码器的参数一致
avcodec_parameters_from_context(st->codecpar, avcodecContext);
//7. 打开输出文件
ret = avio_open(&avFormatContext->pb, outputfile.toStdString().c_str(), AVIO_FLAG_WRITE);
if (ret < 0){
qDebug() << "avio_open failed";
return -6;
}
//8. 写入文件头信息 (包括封装格式的特定信息 流信息(文件头中会包含每个流的参数这些信息是从上下文获取的))
avformat_write_header(avFormatContext, NULL);
//9. 初始化重采样 (设置重采样参数 初始化重采样上下文)
SwrContext* swrContext = nullptr;
//设置重采样参数
swrContext = swr_alloc_set_opts(swrContext, avcodecContext->channel_layout, avcodecContext->sample_fmt,
avcodecContext->sample_rate, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, 44100, 0, 0);
if (!swrContext){
qDebug() << "swr_alloc_set_opts failed";
return -7;
}
ret = swr_init(swrContext); //初始化重采样上下文
if (ret < 0){ //看看参数是否合理
qDebug() << "swr_init failed";
return -7;
}
//10. 准备frame音频帧 (分配音频帧 设置帧参数 为帧分配内存)
AVFrame* frame = nullptr;
frame = av_frame_alloc(); //分配音频帧
//设置frame参数
frame->format = AV_SAMPLE_FMT_FLTP; //帧格式
frame->channels = 2; //通道数
frame->channel_layout = AV_CH_LAYOUT_STEREO; //声道布局 立体声
frame->nb_samples = 1024; //样本点数量
//为帧分配内存
ret = av_frame_get_buffer(frame, 0);
if (ret < 0){
qDebug() << "av_frame_get_buffer failed";
return -8;
}
/*编码阶段*/
//开始读pcm数据 -> frame
int readSize = frame->nb_samples * 2 * 2; //每次需要读的大小
char* pcms = new char[readSize]; //申请内存
//11. 打开输入文件
FILE* fp = fopen(inputfile.toStdString().c_str(), "rb");
if (!fp){
qDebug() << "fopen inputfile failed";
return -9;
}
//开始编码
//12. 循环读数据编码并写入输出文件(读取pcm数据 重采样 编码写入输出文件_
for(;;){
AVPacket pkt;
av_init_packet(&pkt); //初始化数据包Packet存储编码后的音频数据
int len = fread(pcms,1,readSize, fp); //读取 PCM 数据
if (len <= 0){ //读取到文件末尾或发生错误
//刷新编码
avcodec_send_frame(avcodecContext, nullptr);
while (avcodec_receive_packet(avcodecContext, &pkt) != AVERROR_EOF);
//循环接收编码器输出的剩余数据包 直到没有更多数据
break;
}else{
//开始重采样 将 PCM 数据从原始格式转换为编码器所需的格式
const uint8_t* data[1];
data[0] = (uint8_t*) pcms;
//重采样操作
ret = swr_convert(swrContext, frame->data, frame->nb_samples, data, frame->nb_samples);
if (ret <= 0){
qDebug() << "swr_convert failed";
break;
}
// 设置帧的时间戳
static int frame_count = 0;
frame->pts = frame_count++;
//重采样成功 我们编码数据
ret = avcodec_send_frame(avcodecContext, frame);
if (ret < 0){ //编码失败
qDebug() << "avcodec_send_frame failed";
continue; //继续编码下一帧
}
ret = avcodec_receive_packet(avcodecContext, &pkt);
if (ret == 0){
//接收成功 将AVPacket压缩数据写入到输出文件
av_interleaved_write_frame(avFormatContext, &pkt);
}
}
}
//13. 写入文件尾信息
av_write_trailer(avFormatContext);
//14. 刷新编码器
avcodec_send_frame(avcodecContext, nullptr);
//15. 释放资源
fclose(fp);
avio_close(avFormatContext->pb);
avcodec_free_context(&avcodecContext);
avformat_free_context(avFormatContext);
return 0;
}
可以发现pcm转aac的编码好像要比yuv转h264复杂,多了下面一些步骤:一个是要去创建流 以及设置流参数和编码器上下文参数一致 还有一个是要写入文件头信息文件尾信息 还有要进行重采样后才能编码
这些差异源于音频编码的时序敏感性(需要精确到采样点)和容器格式的强制性要求。视频编码由于帧独立性更强,处理流程相对简单。(具体需要更深入的去了解aac编码格式和h264编码格式)
关于为什么需要创建流和让流参数和编码器参数一致,而之前的yuv转h264不用:
H264裸流通过SPS/PPS自带参数,视频帧自带时间戳和关键帧标记,而AAC需要流作为数据载体,流对象存储着时间基(time_base)、编码参数等关键信息,同时要确保封装时使用的参数与编码器一致,不然可能出现采样率/声道数不匹配等问题
关于为什么需要写入文件头和文件尾 :
音频裸流AAC需要ADTS头记录采样率、声道数等元信息,同时ADTS还包含同步字字段,用于定位帧起始位置,用于帧同步而视频裸流(H264)自带帧同步信息 (可以看看aac音频分析那一篇)
可以进行一个对比看看:
- H264裸流(如 .h264 文件)通过NALU(Network Abstraction Layer Unit)单元组织数据
- 每个NALU以 00 00 00 01 或 00 00 01 作为起始码(Sync Byte)
- 关键帧(I帧)自带SPS/PPS参数集,包含:SPS(Sequence Parameter Set),profile_idc/level_idc/帧率/分辨率等;PPS(Picture Parameter Set) 熵编码类型/切片组数等
因此可以直接写入文件,播放器通过解析NALU头即可同步解码
ADTS头是AAC音频流的一部分 :
- ADTS(Audio Data Transport Stream)头是AAC裸流(原始AAC数据)自带的头部信息
- 每个AAC数据帧前都有7-9字节的ADTS头(类似H264的NALU头)
音频帧没有类似视频的关键帧概念,所以需要容器头维护,而文件尾写入用于更新最终的帧计数和时长统计
关于编码前为什么需要重采样:
输入PCM通常是S16格式,而AAC编码器需要FLTP格式,而视频YUV420P可以直接被H264编码器接受
音频重采样还处理了:
- 声道混合/分离
- 采样点对齐(1024 samples/frame)
- 比特深度转换