导读
在后面的咱们应用 FFmpeg 进行相干实际,对音视视频进行理解封装、解码等相干操作,明天咱们持续应用 FFmpeg 进行实际,应用 FFmpeg 进行音视频编码。
工作一:
在后面《FFmpeg 连载 4 - 音频解码》咱们将音频解码成 PCM 并输入到本地文件,明天咱们就把这个输入到本地的 PCM 文件进行读取从新编码成 AAC 音频文件并输入到本地。
工作二:
在《FFmpeg 连载 3 - 视频解码》一节中咱们将视频解码成 YUV 并且输入到本地文件,明天咱们读取这个输入的 YUV 本地文件进行从新编码成 H264 视频文件并输入到本地。
H264 编码规格简介
因为在设置编码器参数时须要用到 profile,所以在这里简略介绍下 H264 的几种 profile 规格。
1、Baseline Profile
反对 I / P 帧,只反对无交织(Progressive)和 CAVLC
个别用于低阶或须要额定容错的利用,比方视频通话、手机视频等即时通信畛域
2、Extended Profile
在 Baseline 的根底上减少了额定的性能,反对流之间的切换,改良误码性能
反对 I /P/B/SP/SI 帧,只反对无交织(Progressive)和 CAVLC
适宜于视频流在网络上的传输场合,比方视频点播
3、Main Profile
提供 I /P/ B 帧,反对无交织(Progressive)和交织(Interlaced),反对 CAVLC 和 CABAC
用于支流消费类电子产品规格如低解码(相对而言)的 MP4、便携的视频播放器、PSP 和 iPod 等。
4、High Profile
最罕用的规格
在 Main 的根底上减少了 8 ×8 外部预测、自定义量化、无损视频编码和更多的 YUV 格局(如 4:4:4)
High 4:2:2 Profile(Hi422P)
High 4:4:4 Predictive Profile(Hi444PP)
High 4:2:2 Intra Profile
High 4:4:4 Intra Profile
用于播送及视频碟片存储(蓝光影片),高清电视的利用
YUV 视频编码
在后面解码的文章中咱们介绍了一组解码的函数 avcodec_send_packet
和avcodec_receive_frame
,同样对于编码也有对应的一组函数,它们是 avcodec_send_frame
和avcodec_receive_packet
,
同样一个的调用 avcodec_send_frame
须要对应多个 avcodec_receive_packet
的接管。
相干代码及正文如下:
VideoEncoder.h
class VideoEncoder {
public:
void encode_yuv_to_h264(const char *yuv_path,const char *h264_path);
};
C++ 实现文件:
#include "VideoEncoder.h"
#include <iostream>
extern "C"{
#include "libavcodec/avcodec.h"
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/opt.h>
}
static FILE *h264_out = nullptr;
void encode_video(AVCodecContext* avCodecContext,AVFrame* avFrame,AVPacket* avPacket){int ret = avcodec_send_frame(avCodecContext,avFrame);
if(ret < 0){std::cout << "yuv 发送编码失败" << std::endl;}
while (true){ret = avcodec_receive_packet(avCodecContext,avPacket);
if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
std::cout << "须要输送更多 yuv 数据" << std::endl;
break;
}
std::cout << "写入文件 h264" << std::endl;
fwrite(avPacket->data,1,avPacket->size,h264_out);
av_packet_unref(avPacket);
}
}
void VideoEncoder::encode_yuv_to_h264(const char *yuv_path, const char *h264_path) {const AVCodec *avCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
avCodecContext->time_base = {1,25};
// 配置编码器参数
avCodecContext->width = 720;
avCodecContext->height = 1280;
avCodecContext->bit_rate = 2000000;
avCodecContext->profile = FF_PROFILE_H264_MAIN;
avCodecContext->gop_size = 10;
avCodecContext->time_base = {1,25};
avCodecContext->framerate = {25,1};
// b 帧的数量
avCodecContext->max_b_frames = 1;
avCodecContext->pix_fmt = AV_PIX_FMT_YUV420P;
// 设置 H264 的编码器参数为提早模式,进步编码品质,然而会造成编码速度降落
av_opt_set(avCodecContext->priv_data,"preset","slow",0);
// 关上编码器
int ret = avcodec_open2(avCodecContext,avCodec, nullptr);
if(ret < 0){std::cout << "编码器关上失败:" << strerror(ret) << std::endl;
// todo 在析构函数中开释资源
return;
}
AVPacket *avPacket = av_packet_alloc();
AVFrame *avFrame = av_frame_alloc();
avFrame->width = avCodecContext->width;
avFrame->height = avCodecContext->height;
avFrame->format = avCodecContext->pix_fmt;
// 为 frame 调配 buffer
av_frame_get_buffer(avFrame,0);
av_frame_make_writable(avFrame);
h264_out = fopen(h264_path,"wb");
// 读取 yuv 数据送入编码器
FILE *input_media = fopen(yuv_path,"r");
if(nullptr == input_media){
std::cout << "输出文件关上失败" << std::endl;
return;
}
int pts = 0;
while (!feof(input_media)){
int64_t frame_size = avFrame->width * avFrame->height * 3 / 2;
int64_t read_size = 0;
// 这里能够自行理解下 ffmpeg 字节对齐的问题
if(avFrame->width == avFrame->linesize[0]){
std::cout << "不存在 padding 字节" << std::endl;
// 读取 y
read_size += fread(avFrame->data[0],1,avFrame->width * avFrame->height,input_media);
// 读取 u
read_size += fread(avFrame->data[1],1,avFrame->width * avFrame->height / 4,input_media);
// 读取 v
read_size += fread(avFrame->data[2],1,avFrame->width * avFrame->height / 4,input_media);
} else{
std::cout << "存在 padding 字节" << std::endl;
// 须要对 YUV 重量进行逐行读取
for (int i = 0; i < avFrame->height;i++) {
// 读取 y
read_size += fread(avFrame->data[0] + i * avFrame->linesize[0],1,avFrame->width,input_media);
}
// 读取 u 和 v
for (int i = 0; i < avFrame->height / 2; i++) {read_size += fread(avFrame->data[1] + i * avFrame->linesize[1],1,avFrame->width / 2,input_media);
read_size += fread(avFrame->data[2] + i * avFrame->linesize[2],1,avFrame->width / 2,input_media);
}
}
pts += (1000000 / 25);
avFrame->pts = pts;
if(read_size != frame_size){std::cout << "读取数据有误:" << std::endl;}
encode_video(avCodecContext,avFrame,avPacket);
}
// 冲刷编码器
encode_video(avCodecContext, nullptr,avPacket);
fflush(h264_out);
}
须要留神的是在读取 YUV 数据填充 AVFrame 时须要辨别开释存在字节对齐的问题。
AAC 简略介绍
AAC(Advanced Audio Coding,译为:高级音频编码),是由 Fraunhofer IIS、杜比实验室、AT&T、Sony、Nokia 等公司共同开发的有损音频编码和文件格式。
AAC 相较于 MP3 的有更多的改良:
1、更多的采样率抉择:8kHz ~ 96kHz,MP3 为 16kHz ~ 48kHz
2、更高的声道数下限:48 个,MP3 在 MPEG- 1 模式下为最多双声道,MPEG- 2 模式下 5.1 声道
3、改良的压缩性能:以较小的文件大小提供更高的品质
4、…… 等等等
AAC 是一个宏大家族,为了适应不同场合的须要,它有很多种规格可供选择。上面简略介绍几种常见的规格:
1、AAC Main:主规格
2、AAC LC:低复杂度规格(Low Complexity),适宜中等比特率,比方 96kbps ~ 192kbps 之间。当初的手机比拟常见的 MP4 文件中的音频局部应用了该规格
3、AAC HE:高效率规格(High Efficiency),适宜低比特率,HE 有 v1 和 v2 两个版本,其中 v1 适宜 48kbps ~ 64kbps;v2 适宜低于 32kbps,可在低至 32kbps 的比特率下提供靠近 CD 品质的声音。
貌似 FFmpeg 自带的 AAC 编码器不反对这个???
PCM 音频编码
间接上代码吧 …
/**
* 将 PCM 编码成 AAC
*/
extern "C"{
#include <libavcodec/avcodec.h>
#include <libavutil/log.h>
#include <libavformat/avformat.h>
#include <libavutil/samplefmt.h>
#include <libavutil/common.h>
#include <libavutil/channel_layout.h>
}
class AudioEncoder{
public:
/**
* 保持编码器开释反对该采样格局
* @param codec
* @param sample_fmt
* @return
*/
bool check_sample_fmt(const AVCodec *codec, enum AVSampleFormat sample_fmt)
{
const enum AVSampleFormat *p = codec->sample_fmts;
while (*p != AV_SAMPLE_FMT_NONE) { // 通过 AV_SAMPLE_FMT_NONE 作为结束符
if (*p == sample_fmt)
return true;
p++;
}
return false;
}
/**
* 查看编码器开释反对该采样率
* @param codec
* @param sample_rate
* @return
*/
bool check_sample_rate(const AVCodec *codec, const int sample_rate)
{
const int *p = codec->supported_samplerates;
while (*p != 0) {// 0 作为退出条件,比方 libfdk-aacenc.c 的 aac_sample_rates
printf("%s support %dhz\n", codec->name, *p);
if (*p == sample_rate)
return true;
p++;
}
return false;
}
void encode_pcm_to_aac(const char *pcm_path,const char *aac_path){av_log_set_level(AV_LOG_DEBUG);
const AVCodec *avCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
avCodecContext->sample_rate = 44100;
// 默认的 aac 编码器输出的 PCM 格局为:AV_SAMPLE_FMT_FLTP
avCodecContext->sample_fmt = AV_SAMPLE_FMT_FLTP;
avCodecContext->channel_layout = AV_CH_LAYOUT_STEREO;
avCodecContext->bit_rate = 128 * 1024;
avCodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
avCodecContext->channels = av_get_channel_layout_nb_channels(avCodecContext->channel_layout);
avCodecContext->profile = FF_PROFILE_MPEG2_AAC_HE;
//ffmpeg 默认的 aac 是不带 adts,而 fdk_aac 默认带 adts,这里咱们强制不带
avCodecContext->flags = AV_CODEC_FLAG_GLOBAL_HEADER;
/* 检测反对采样格局反对状况 */
if (!check_sample_fmt(avCodec, avCodecContext->sample_fmt)) {
av_log(nullptr, AV_LOG_DEBUG,"Encoder does not support sample format %s",
av_get_sample_fmt_name(avCodecContext->sample_fmt));
return;
}
if (!check_sample_rate(avCodec, avCodecContext->sample_rate)) {av_log(nullptr, AV_LOG_DEBUG,"Encoder does not support sample rate %d", avCodecContext->sample_rate);
return;
}
AVFormatContext *avFormatContext = avformat_alloc_context();
const AVOutputFormat *avOutputFormat = av_guess_format(nullptr,aac_path, nullptr);
avFormatContext->oformat = avOutputFormat;
AVStream *aac_stream = avformat_new_stream(avFormatContext,avCodec);
// 关上编码器
int ret = avcodec_open2(avCodecContext,avCodec, nullptr);
if(ret < 0){char error[1024];
av_log(nullptr, AV_LOG_DEBUG,"编码器关上失败: %s",
av_strerror(ret,error,1024));
}
// 编码信息拷贝,放在关上编码器之后
ret = avcodec_parameters_from_context(aac_stream->codecpar,avCodecContext);
// 关上输入流
avio_open(&avFormatContext->pb,aac_path,AVIO_FLAG_WRITE);
ret = avformat_write_header(avFormatContext, nullptr);
if(ret < 0){char error[1024];
av_log(nullptr, AV_LOG_DEBUG,"avformat_write_header fail: %s",
av_strerror(ret,error,1024));
return;
}
AVPacket *avPacket = av_packet_alloc();
AVFrame *avFrame = av_frame_alloc();
avFrame->channel_layout = avCodecContext->channel_layout;
avFrame->format = avCodecContext->sample_fmt;
avFrame->channels = avCodecContext->channels;
// 每次送多少数据给编码器 aac 是 1024 个采样点
avFrame->nb_samples = avCodecContext->frame_size;
// 调配 buffer
av_frame_get_buffer(avFrame,0);
// 每帧数据大小
int per_sample = av_get_bytes_per_sample(static_cast<AVSampleFormat>(avFrame->format));
FILE *pcm_file = fopen(pcm_path,"rb");
int64_t pts = 0;
while (!feof(pcm_file)){
// 设置可写
ret = av_frame_make_writable(avFrame);
// 从输出文件中交替读取各个声道的数据
for (int i = 0; i < avFrame->nb_samples; ++i) {for (int ch = 0; ch < avCodecContext->channels; ++ch) {fread(avFrame->data[ch] + per_sample * i,1,per_sample,pcm_file);
}
}
// 设置 pts 应用采样率作为 pts 的单位,具体换算成秒 pts*1/ 采样率
pts += avFrame->nb_samples;
avFrame->pts = pts;
if(ret < 0){char error[1024];
av_strerror(ret,error,1024);
av_log(nullptr, AV_LOG_DEBUG,"av_samples_fill_arrays fail: %s",error);
return;
}
// 送去编码
ret = avcodec_send_frame(avCodecContext,avFrame);
if(ret < 0){char error[1024];
av_strerror(ret,error,1024);
av_log(nullptr, AV_LOG_DEBUG,"avcodec_send_frame fail: %s",
error);
return;
}
while (true){ret = avcodec_receive_packet(avCodecContext,avPacket);
if(ret == AVERROR_EOF || ret == AVERROR(EAGAIN)){
// 须要更多数据
av_log(nullptr, AV_LOG_DEBUG,"avcodec_receive_packet need more data");
break;
} else if(ret < 0){char error[1024];
av_log(nullptr, AV_LOG_DEBUG,"avcodec_receive_packet fail: %s",
av_strerror(ret,error,1024));
break;
} else{
avPacket->stream_index = aac_stream->index;
av_interleaved_write_frame(avFormatContext,avPacket);
av_packet_unref(avPacket);
}
}
}
av_write_trailer(avFormatContext);
// 敞开
avio_close(avFormatContext->pb);
av_packet_free(&avPacket);
av_frame_free(&avFrame);
}
};
todo 与思考
1、在下面的例子中咱们编码进去的视频文件,如果通过 ffprobe 命令来看出相干媒体信息的话,是没有时长的,也就是说咱们尽管编码胜利了,然而视频的工夫戳失落了,如果想要编码出正确的工夫那该如何解决呢?
编码进去的音频的时长是对的,能够参考下音频是怎么计算的。
2、无论是编码器还是解码器,在没有更多数据输送时都应该发送空的数据包进行数据冲刷,以达到将解码器或编码器外部所有数据获取完的目标 …
举荐浏览
FFmpeg 连载 1 - 开发环境搭建
FFmpeg 连载 2 - 拆散视频和音频
FFmpeg 连载 3 - 视频解码
FFmpeg 连载 4 - 音频解码
关注我,一起提高,人生不止 coding!!!