直播推流之 2: ffmpeg借助rtmpdump推送rtmp流,编解码部分

160 阅读11分钟

项目背景介绍

使用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的时候推送它。