项目背景介绍
使用ffmpeg集成rtmpdump项目的librtmp库,推送rtmp流。视频编码采用h264,音频采用AAC。本项目的开发过程是一步一步来的,前面有一些编码,采集的准备知识。如果是0基础,建议从前面的博客开始学起
环境
- VS 版本: Visual Studio Professional 2022 (64 位)
- QT 版本: 5.12.0
- c++ 语言
- ffmpeg3.4 window编译的动态库
- fdk-aac v0.1.6,编译后集成到ffmpeg
- x264库
- librtmp库
- srs rtmp服务
代码
- 源码仓库:
https://github.com/SnailCoderGu/MediaPush.git
- 讲解视频地址: 视频讲解地址
开发过程
开发过程遇到问题总结
-
在一开始,我准备直接使用ffmpeg来做rtmp的封装推流,但是实际开发完后,发现ffmpeg推送的metadata包,始终不是amf协议,导致服务解析不到。这个我怎么看出来的呢,我先用obs推送了正常的流,用wireshark抓包分析了一下推流的数据,然后对比了一下我自己的推送的。 后面我集成librtmp,具体怎么集成,可以翻看我前面的博客。解决这个问题
-
第二个遇到的问题是,我在推送过程,srs服务始终报解析不到sps,pps。具体原因其实ffmpeg会在writehead的时候,推送的第一针视频数据会是sps,pps。但是这个sps,pps,要怎么来呢,要来自于编码器。这个是个连贯过程。后面说明
-
第三个问题,就是编码器,怎么将sps,pps,对于音频就是ASC。这些携带编码信息的数据。给ffmpeg封装到rtmp里面去。我之前都是用librmtp的源码推送,是adts和sps,pps放在每个I帧。和ffmpeg推送有一些区别。
-
第四个就是rtmp推流的时间错,这个很重要,很多时候决定了播放的时候,卡不卡
视频
推送rtmp,视频采集过程没有做什么修改,所以采集过程,就不赘述了。从
void MediaPushWindow::start()
开始讲起
start方法
void MediaPushWindow::start()
{
ui.actionStart->setEnabled(false);
ui.actionStop->setEnabled(true);
ui.actionSettings->setEnabled(false);
start_flag = true;
audio_encoder_data = new unsigned char[1024 * 2 * 2];
video_encoder_data = new unsigned char[1024 * 1024];
m_mic->OpenWrite();
aacEncoder.reset(new AacEncoder());
InitAudioEncode();
if (!ffVideoEncoder)
{
#if USE_FFMPEG_VIDEO_ENCODE
ffVideoEncoder.reset(new VideoEncodeFF());
ffVideoEncoder->InitEncode(width_, height_, 25, 2000000, "main");
#else
ffVideoEncoder.reset(new VideoEncoderX());
ffVideoEncoder->InitEncode(width_, height_, 25, 2000000, "42801f");
#endif
}
rtmpPush.reset(new RtmpPush());
rtmpPush->OpenFormat("rtmp://192.168.109.128:1935/live/test");
VideoEncoder::VCodecConfig& vcode_info = ffVideoEncoder->GetCodecConfig();
rtmpPush->InitVideoCodePar(static_cast<AVMediaType>(vcode_info.codec_type),
static_cast<AVCodecID>(vcode_info.codec_id),
vcode_info.width, vcode_info.height, vcode_info.fps, vcode_info.format,
ffVideoEncoder->GetExterdata(), ffVideoEncoder->GetExterdataSize());
AudioEncoder::ACodecConfig& acode_info = aacEncoder->GetCodecConfig();
rtmpPush->InitAudioCodecPar(static_cast<AVMediaType>(acode_info.codec_type),
static_cast<AVCodecID>(acode_info.codec_id),
acode_info.sample_rate, acode_info.channel, acode_info.format,
aacEncoder->GetExterdata(),aacEncoder->GetExterdataSize());
rtmpPush->WriteHeader();
#ifdef WRITE_CAPTURE_YUV
if (!yuv_out_file) {
yuv_out_file = fopen("ouput.yuv", "wb");
if (yuv_out_file == nullptr)
{
qDebug() << "Open yuv file failed";
}
}
if (!rgb_out_file) {
rgb_out_file = fopen("output.rgb", "wb");
if (rgb_out_file == nullptr)
{
qDebug() << "Open rgb file failed";
}
}
#endif // WRITE_CAPTURE_YUV
}
这个方式,是在我们点击菜单开始按钮。在这里创建编码器和推流器。 这里暂时用ffmpeg编码的。x264直接编码的,我还没适配。 aacEncoder 是音频编码器 ffVideoEncoder 是视频编码器 rtmpPush 是推流器
rtmpPush->OpenFormat("rtmp://192.168.109.128:1935/live/test");
是连接rtmp服务
rtmpPush->WriteHeader();
推送头信息,包括metadata,和第一个带音视频编码信息包。
视频编码
- VideoEncodeFF 的 InitEncode 方法
int VideoEncodeFF::InitEncode(int width, int height, int fps, int bitrate, const char* profile)
{
if (width <= 0 || height <= 0)
return -1;
int ret = 0;
av_register_all();
AVCodec* codec = NULL;
//while ((codec = av_codec_next(codec))) {
// if (codec->encode2 && codec->type == AVMediaType::AVMEDIA_TYPE_VIDEO) {
// qDebug() << "Codec Name :" << codec->name;
// qDebug() << "Type: " << av_get_media_type_string(codec->type);
// qDebug() << "Description: " << (codec->long_name ? codec->long_name : codec->name);
// qDebug() << "---";
// }
//}
codec = avcodec_find_encoder_by_name("libx264");
//codec = avcodec_find_encoder(AVCodecID::AV_CODEC_ID_H264);
if (codec == NULL) {
//qDebug() << "cannot find video codec id: " << codec->id;
return -1;
}
qDebug() << "codec name: " << codec->name;
qDebug() << "codec long name: " << codec->long_name;
videoCodecCtx = avcodec_alloc_context3(codec);
if (videoCodecCtx == nullptr)
{
qDebug() << "alloc context3 afild: " << codec->long_name;
return -1;
}
pkt = av_packet_alloc();
if (!pkt)
exit(1);
// 设置编码参数
videoCodecCtx->bit_rate = bitrate;
videoCodecCtx->width = width;
videoCodecCtx->height = height;
videoCodecCtx->time_base = { 1,fps };
videoCodecCtx->framerate = { fps,1 };
videoCodecCtx->gop_size = 10;
videoCodecCtx->max_b_frames = 0;
videoCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
std::string strProfile = profile;
if (strProfile == "main")
{
videoCodecCtx->profile = FF_PROFILE_H264_MAIN;
}
else if (strProfile == "base")
{
videoCodecCtx->profile = FF_PROFILE_H264_BASELINE;
}
else
{
videoCodecCtx->profile = FF_PROFILE_H264_HIGH;
}
ret = av_opt_set(videoCodecCtx->priv_data, "preset", "ultrafast", 0);
if (ret != 0)
{
qDebug() << "set opt preset error";
}
ret = av_opt_set(videoCodecCtx->priv_data,"tune","zerolatency", 0);
if (ret != 0)
{
qDebug() << "set opt tune error";
}
//x264_param_t* x264_param = (x264_param_t*)videoCodecCtx->priv_data;
//x264_param_default_preset(x264_param, "ultrafast", "zerolatency");
//x264_param_apply_profile(x264_param, "high");
//决定了是否在每个i帧前面带sps,pps,如果全局head ,videoCodecCtx->extradata里面就会带sps,pps
videoCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
// 打开编码器
ret = avcodec_open2(videoCodecCtx, codec, NULL);
if (ret < 0) {
qDebug() << "avcodec_open2 filed ";
exit(1);
}
frame = av_frame_alloc();
if (!frame) {
fprintf(stderr, "Could not allocate video frame\n");
exit(1);
}
frame->format = videoCodecCtx->pix_fmt;
frame->width = videoCodecCtx->width;
frame->height = videoCodecCtx->height;
ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
fprintf(stderr, "Could not allocate the video frame data\n");
exit(1);
}
#ifdef WRITE_CAPTURE_264
if (!h264_out_file) {
h264_out_file = fopen("ouput.264", "wb");
if (h264_out_file == nullptr)
{
qDebug() << "Open 264 file failed";
}
}
#endif // WRITE_CAPTURE_264
memset(frame->data[0], 0, videoCodecCtx->height * frame->linesize[0]); // Y
memset(frame->data[1], 0, videoCodecCtx->height / 2 * frame->linesize[1]); // U
memset(frame->data[2], 0, videoCodecCtx->height / 2 * frame->linesize[2]); // V
// 设置全局头部标志
if (videoCodecCtx->flags & AV_CODEC_FLAG_GLOBAL_HEADER) {
}
else
{
//没有设置全局头,那么就编码一帧数据,获取sps,pps。
// 发送空帧到编码器以触发 SPS 和 PPS 的生成
if (avcodec_send_frame(videoCodecCtx, frame) < 0) {
qDebug() << "Failed to send frame";
av_frame_free(&frame);
avcodec_free_context(&videoCodecCtx);
return -1;
}
ret = avcodec_receive_packet(videoCodecCtx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
qDebug() << "Error encoding audio frame " << ret;
return 0;
}
else if (ret < 0) {
qDebug() << "Error encoding audio frame" << ret;
exit(1);
}
#ifdef WRITE_CAPTURE_264
fwrite(pkt->data, 1, pkt->size, h264_out_file);
#endif // WRITE_CAPTURE_264
int frame_type = pkt->data[4] & 0x1f;
if (frame_type == 7 && receive_first_frame)
{
//sps,pps,
CopySpsPps(pkt->data, pkt->size);
receive_first_frame = false;
}
}
// 保存编码信息,用于外部获取
codec_config.codec_type = static_cast<int>(AVMEDIA_TYPE_VIDEO);
codec_config.codec_id = static_cast<int>(AV_CODEC_ID_H264);
codec_config.width = videoCodecCtx->width;
codec_config.height = videoCodecCtx->height;
codec_config.format = videoCodecCtx->pix_fmt;
codec_config.fps = fps;
return 0;
}
代码和之前讲解h264编码基本一致,为了rtmp推流,有以下一些修改:
//决定了是否在每个i帧前面带sps,pps,如果全局head ,videoCodecCtx->extradata里面就会带sps,pps
videoCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
- AV_CODEC_FLAG_GLOBAL_HEADER的标志启用后,就会创建一个全局的sps,sps信息数据。这个数据存放在 videoCodecCtx->extradata 里面。如果不启用这个标志位,那么extradata为空,而且每个I帧前面会有sps,pps。 如果没有启用全局的头信息,那么可以 在编码器启动后,主动编码一帧数据,就可以获取一个sps,pps信息。后面的
// 设置全局头部标志
if (videoCodecCtx->flags & AV_CODEC_FLAG_GLOBAL_HEADER) {
}
else
{
。。。。
}
有个取sps,pps的逻辑,
-
VideoEncodeFF 的 GetExterdata 方法 这个方法返回了编码器携带的sps,pps数据。后面rtmppush推流会用到。
-
VideoEncodeFF 的 CopySpsPps 方法 是从一帧I帧数据中取出前面的sps,pps信息的方法,这里作为一个代码理解的吧。实际用带头信息的情况就够了
-
VideoEncodeFF 的 Encode 方法
unsigned int VideoEncodeFF::Encode(unsigned char* src_buf, unsigned char* dst_buf)
{
int ylen = frame->linesize[0] * videoCodecCtx->coded_height;
int ulen = frame->linesize[1] * videoCodecCtx->coded_height /2 ;
int vlen = frame->linesize[2] * videoCodecCtx->coded_height / 2;
memcpy(frame->data[0], src_buf, ylen);
memcpy(frame->data[1], src_buf + ylen, ulen);
memcpy(frame->data[2], src_buf + ylen+ulen, vlen);
frame->pts = pts;
pts++;
int ret = avcodec_send_frame(videoCodecCtx, frame);
if (ret < 0) {
fprintf(stderr, "Error sending a frame for encoding\n");
exit(1);
}
ret = avcodec_receive_packet(videoCodecCtx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return 1;
else if (ret < 0) {
fprintf(stderr, "Error during encoding\n");
exit(1);
}
#ifdef WRITE_CAPTURE_264
fwrite(pkt->data, 1, pkt->size, h264_out_file);
#endif // WRITE_CAPTURE_264
if (dst_buf != nullptr)
{
memcpy(dst_buf, pkt->data, pkt->size);
}
ret = pkt->size;
av_packet_unref(pkt);
return ret;
}
基本没有区别。主要就是,当 dst_buf存在的时候,把pkt编码的数据拷贝进去。然后要返回实际编码后的数据长度 pkt->size
音频
音频的处理会复杂一点
- 采集
qint64 AudioCapture::writeData(const char* data, qint64 len)
{
// 在这里处理音频数据,例如保存到文件、进行处理等
//qDebug() << "Received audio data. Size:" << len;
CaculateLevel(data,len);
if (write_flag)
{
int pre_remian = nb_swr_remain + len; //预计的当前拥有的数据总长度
char* data_curr = const_cast<char*>(data);
int len_curr = len;
while (true){
if (pre_remian < nb_sample_size)
{
memcpy(src_swr_data + nb_swr_remain, data_curr, len);
nb_swr_remain = pre_remian;
break;
}
else
{
int missing_len = nb_sample_size - nb_swr_remain;
memcpy(src_swr_data + nb_swr_remain, data_curr, missing_len);
PaceToResample(src_swr_data, nb_sample_size);
nb_swr_remain = 0;
pre_remian = pre_remian - nb_sample_size; //消费了一个单位
data_curr = data_curr + missing_len;
len_curr = len - missing_len;
}
}
}
return len;
}
writeData 是系统采集出来的数据
在这里,我们做了一次数据重组,将采集的pcm数据组成 nb_sample_size 长度大小的。 重组数据的原理,请参考我前面的博客。
void AudioCapture::PaceToResample(const char* data, qint64 len)
{
#ifdef WRITE_RAW_PCM_FILE
fwrite(data, 1, len, out_raw_pcm_file);
#endif
if (len != nb_sample_size)
{
qDebug() << "Reorganize resample dst data len is wrong " << len;
return;
}
//消费数据
m_pSwr->WriteInput(src_swr_data, nb_sample_size); //写如数据
char* result_data = nullptr;
int rlen = m_pSwr->SwrConvert(&result_data); //执行重采样
if (dst_enc_data)
{
PaceToEncode(result_data, rlen);
}
}
PaceToResample 方法里面,我对数据做了重采样。由于我们修改了采样率。所以重采样后。为了保证时长一致。48000HZ的1024个采样,就变成了44100的941个采样。
重采样后的数据,我们有送入PackeToEncode方法。
void AudioCapture::PaceToEncode(char* data, qint64 len)
{
//再重行组织一次1024长度给编码器
int pre_remian = nb_enc_remain + len; //预计的当前拥有的数据总长度
char* data_curr = data;
int len_curr = len;
while (true) {
if (pre_remian < dst_enc_nb_sample_size)
{
memcpy(dst_enc_data + nb_enc_remain, data_curr, len);
nb_enc_remain = pre_remian;
break;
}
else
{
int missing_len = dst_enc_nb_sample_size - nb_enc_remain;
memcpy(dst_enc_data + nb_enc_remain, data_curr, missing_len);
emit aframeAvailable(dst_enc_data, dst_enc_nb_sample_size);
nb_enc_remain = 0;
pre_remian = pre_remian - dst_enc_nb_sample_size; //消费了一个单位
data_curr = data_curr + missing_len;
len_curr = len - missing_len;
}
}
}
由于 AAC编码器一般采用的1024个采样编码一帧。所以这里,我们又再一次做数据重组。把941,941的数据重组为dst_enc_nb_sample_size(1024) 长度的数据。 然后通过信号槽抛送出去。
- 创建编码器
aacEncoder.reset(new AacEncoder());
InitAudioEncode();
同样在 void MediaPushWindow::start() 方法里面我们创建了音频编码器,aacEncoder
- 音频编码器初始化
void MediaPushWindow::InitAudioEncode()
{
int sample_rate = m_mic->format().sample_rate;
int channel_layout = m_mic->format().chanel_layout;
AVSampleFormat smaple_fmt = m_mic->format().sample_fmt;
aacEncoder->InitEncode(sample_rate, 96000, smaple_fmt, channel_layout);
m_mic->InitDecDataSize(aacEncoder->frame_byte_size);
}
这里获取了采集器里面的一些音频参数,输入到编码器。然后有将音频编码器一帧音频字节长度输入到采集器里面。采集器里面就是根据这个大小去组装送入编码器的pcm数据
- 音频编码器初始化
int AacEncoder::InitEncode(int sample_rate, int bit_rate, AVSampleFormat sample_fmt, int chanel_layout)
{
//avcodec_register_all();
av_register_all();
const AVCodec* codec = nullptr;
//while ((codec = av_codec_next(codec))) {
// if (codec->encode2 && codec->type == AVMediaType::AVMEDIA_TYPE_AUDIO) {
// qDebug() << "Codec Name :" << codec->name;
// qDebug() << "Type: " << av_get_media_type_string(codec->type);
// qDebug() << "Description: " << (codec->long_name ? codec->long_name : codec->name);
// qDebug() << "---";
// }
//}
/* find the MP2 encoder */
codec = avcodec_find_encoder_by_name("libfdk_aac");
//codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
if (!codec) {
fprintf(stderr, "Codec not found\n");
exit(1);
}
qDebug() << "codec name: " << codec->name;
qDebug() << "codec long name: " << codec->long_name;
const enum AVSampleFormat* p = codec->sample_fmts;
while (*p != AV_SAMPLE_FMT_NONE) {
qDebug() << "supoort codec fmt : " << av_get_sample_fmt_name(*p);
p++;
}
audioCodecCtx = avcodec_alloc_context3(codec);
if (!audioCodecCtx) {
fprintf(stderr, "Could not allocate audio codec context\n");
exit(1);
}
//打印看到只支持 AV_SAMPLE_FMT_S16,所以这里写死
audioCodecCtx->sample_rate = sample_rate;
audioCodecCtx->channel_layout = chanel_layout;
audioCodecCtx->channels = av_get_channel_layout_nb_channels(audioCodecCtx->channel_layout);
audioCodecCtx->sample_fmt = sample_fmt;
audioCodecCtx->bit_rate = bit_rate;
audioCodecCtx->profile = FF_PROFILE_AAC_HE;
//检查是否支持fmt
if (!check_sample_fmt(codec, audioCodecCtx->sample_fmt)) {
//fprintf(stderr, "Encoder does not support sample format %s",av_get_sample_fmt_name(audioCodecCtx->sample_fmt));
qDebug() << "Encoder does not support sample format " << av_get_sample_fmt_name(audioCodecCtx->sample_fmt);
exit(1);
}
if (!check_sample_rate(codec, audioCodecCtx->sample_rate)) {
//fprintf(stderr, "Encoder does not support sample format %s",av_get_sample_fmt_name(audioCodecCtx->sample_fmt));
qDebug() << "Encoder does not support sample rate " << audioCodecCtx->sample_rate;
exit(1);
}
// Set ADTS flag,设置后,就没有单个adts头了
audioCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
/* open it */
if (avcodec_open2(audioCodecCtx, codec, NULL) < 0) {
fprintf(stderr, "Could not open codec\n");
exit(1);
}
//编码输入数据的长度 1024* channel*fmt
frame_byte_size = audioCodecCtx->frame_size * audioCodecCtx->channels * 2;
pkt = av_packet_alloc();
if (!pkt) {
fprintf(stderr, "could not allocate the packet\n");
exit(1);
}
/* frame containing input raw audio */
frame = av_frame_alloc();
if (!frame) {
fprintf(stderr, "Could not allocate audio frame\n");
exit(1);
}
frame->nb_samples = audioCodecCtx->frame_size;
frame->format = audioCodecCtx->sample_fmt;
frame->channel_layout = audioCodecCtx->channel_layout;
/* allocate the data buffers */
int ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
fprintf(stderr, "Could not allocate audio data buffers\n");
exit(1);
}
#ifdef WRITE_CAPTURE_AAC
if (!aac_out_file) {
aac_out_file = fopen("ouput.aac", "wb");
if (aac_out_file == nullptr)
{
}
}
#endif // WRITE_CAPTURE_AAC
//保存编码信息
codec_config.codec_type = static_cast<int>(AVMediaType::AVMEDIA_TYPE_AUDIO);
codec_config.codec_id = static_cast<int>(AV_CODEC_ID_AAC);
codec_config.sample_rate = audioCodecCtx->sample_rate;
codec_config.channel = audioCodecCtx->channels;
codec_config.format = audioCodecCtx->sample_fmt;
return 0;
}
同样,音频编码也有 AV_CODEC_FLAG_GLOBAL_HEADER标志位。设置后,就不能单个加adts头了。 它会在audioCodecCtx->extradata 里面存放编ASC(AudioSpecificConfig) 信息。里面的信息可以用于解码。所以再rtmp推流的时候。会在第针视频tag的时候推送它。