部标jt1078实时音视频,音视频同步推流

上篇讲了视频推流部分,这一篇主要针对1078协议讲音频部分和最后音视频同步处理。

根据jt1078里表12看到,音频编码还是比较多的,我到现在遇到的终端下发的音频数据编码格式有4种,G711A,G711U,G726,ADPCMA。

.部标jt1078实时音视频,音视频同步推流_第1张图片

音频也需要有一个输入,跟视频一样,要调用avformat_open_input函数,起初本以为可以使用一个队列,音视频数据包都放在一个队列里,调用一次avformat_open_input,如果是文件的话就可以,但数据流不行,因为avformat_open_input无法识别出音频的编码格式。所以使用了两个队列,一个视频流队列,一个音频流队列。经过搜索资料,g711编码的codecs(ffmpeg -codecs)是需要有编码ID的,g711a的ID对应着name是pcm_alaw,g711u的ID对应着name是pcm_mulaw,但format没有(ffmpeg -formats)。

AVInputFormat *iformat = av_find_input_format("pcm_alaw");

这段代码iformat 返回的是个空指针。

int ret = avformat_open_input(&ictx_a, NULL, iformat, &opts);

到这里就会返回出错,最后发现format里有对应codecs的格式,我就在想,这个会不会是一一对应的呢,因为alaw对应的格式就是pcm a-law,本来抱着试试的心态,没想到居然通过了。所以这里就是

AVInputFormat *iformat = av_find_input_format("alaw");

g711u使用相同的方法 

AVInputFormat *iformat = av_find_input_format("mulaw");

 之后的操作就和视频一样了,avformat_open_input --> avformat_find_stream_info;

创建输出上下文里添加音频

       for (i = 0; i < ictx_a->nb_streams; i++) {
			//Create output AVStream according to input AVStream
			if (ictx_a->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
				AVStream *in_stream = ictx_a->streams[i];
				
				AVStream *out_stream = avformat_new_stream(*octx, in_stream->codec->codec);
				if (!out_stream) {	
					ret = AVERROR_UNKNOWN;
				}
				
				//Copy the settings of AVCodecContext
				if (avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar) < 0) {
					av_log(NULL, AV_LOG_ERROR, "(audio)avcodec_parameters_copy failed! err_code = %d   index = %d\n",ret, index);

				}
				out_stream->codec->codec_tag = 0;
				out_stream->codecpar->codec_tag = 0;
				if ((*octx)->oformat->flags & AVFMT_GLOBALHEADER)
				{
					out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
				}

			}
		}

 好了,到现在,音频数据格式应该已经成功添加到flv容器里了。现在才是踩坑的开始。在我们处理音视频同步之前,我们需要了解g711的编码格式,采样率等一些参数,如下两篇文章。

计算G711语音的打包长度和RTP里timestamp(时间戳)的增长量

音频采样率与时间戳的计算

如果你们有终端厂家的支持,直接问就好,我没有,就是靠猜出来的。我处理数据包,知道音频的格式是g711a,知道音频数据包数据体的大小是320字节,知道音频的时间戳,每个音频数据包都相隔40ms,所以根据计算G711语音的打包长度和RTP里timestamp(时间戳)的增长量这篇文章,知道g711a的采样率是8000HZ,比特率是64Kbit/s。所以计算出我们的音频是每秒25帧。如果数据流是这样的,视频I帧 > 视频P帧> 音频帧 > 音频帧 > 视频P帧 > 视频P帧 < 音频帧 < 音频帧< 视频P帧 ......我们在同步的时候要把时间戳同步好,

部标jt1078实时音视频,音视频同步推流_第2张图片

核心代码如下:

    int64_t cur_pts_v = 0, cur_pts_a = 0;
	int video_end = 1, audio_end = 1;
	AVRational time_base_q = { 1, AV_TIME_BASE };
	int64_t total_pts = 0;
		rtmp_logger->info("音视频同步推流开始。。。");
		while (video_end || audio_end) {
			AVFormatContext *ifmt_ctx;
			int stream_index = 0;
			AVStream *in_stream, *out_stream;
			AVRational time_base_v = ictx_v->streams[videoindex]->time_base;
			AVRational time_base_a = ictx_a->streams[audioindex]->time_base;
			if (video_end &&
				(!audio_end || av_compare_ts(cur_pts_v, time_base_q, cur_pts_a, time_base_q) <= 0)) {
				ifmt_ctx = ictx_v;
				stream_index = videoindex_out;
				//printf("写入视频帧\n");
				//获取解码前数据
				if ((ret = av_read_frame(ifmt_ctx, &pkt)) >= 0) {
					int send_err = 0;
					int f_seek = 1;
					int h264_fps = av_q2d(ifmt_ctx->streams[videoindex]->r_frame_rate);
					if (h264_fps < 25)
					{
						f_seek = 1;
					}
					else
					{
						double f = (double)this->time_delay_v / 40;
						f_seek = round(f);
						if (f_seek == 0)
						{
							f_seek = 1;
						}
					}
					for (int k = 0; k < f_seek; k++)
					{
						in_stream = ifmt_ctx->streams[pkt.stream_index];
						out_stream = octx[OutPutType::TYPE_RTMP]->streams[stream_index];
						if (pkt.stream_index == videoindex) {

							AVRational time_base1 = in_stream->time_base;
							AVRational r_framerate1 = ifmt_ctx->streams[videoindex]->r_frame_rate;
							//Duration between 2 frames (us)
							int64_t calc_duration = (double)AV_TIME_BASE *(1 / av_q2d(r_framerate1));

							pkt.pts = av_rescale_q(frame_index*calc_duration, time_base_q, time_base1);
							pkt.dts = pkt.pts;
							pkt.duration = av_rescale_q(calc_duration, time_base_q, time_base1);

							frame_index++;
							cur_pts_v = frame_index * calc_duration;
							//Delay
							int64_t pts_time = av_rescale_q(pkt.dts, time_base1, time_base_q);
							int64_t now_time = av_gettime() - start_time;
							if (pts_time > now_time)
							{
								av_usleep(pts_time - now_time);
								//printf("音视频推流 视频睡眠一段时间\n");
							}
						}
						pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
						pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
						pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
						pkt.pos = -1;
						pkt.stream_index = stream_index;
						total_pts = pkt.pts;

						printf("Write 1 Packet. size:%5d\tpts:%lld\n", pkt.size, pkt.pts);
						//Write
						ret = av_write_frame(octx[OutPutType::TYPE_RTMP], &pkt);
						if (ret < 0) {
							av_log(NULL, AV_LOG_ERROR, "(音视频)rtmp发送数据包出错err code = %d\n", ret);
							rtmp_logger->error("(音视频)rtmp发送数据包出错err code = {}!", ret);
							av_packet_unref(&pkt);
							send_err = 1;
							break;
						}

					}
					if (send_err == 1)
					{
						break;
					}
				}
				else {
					video_end = 0;
					continue;
				}
			}
			else {
				ifmt_ctx = ictx_a;
				stream_index = audioindex_out;
				//printf("写入音频帧\n");
				if (av_read_frame(ifmt_ctx, &pkt) >= 0) {
					
					/*ret = av_bsf_send_packet(bsf_ctx, &pkt);
					if (ret < 0)
					{
						av_log(NULL, AV_LOG_ERROR, "av_bsf_send_packet error!\n");
						continue;
					}
					ret = av_bsf_receive_packet(bsf_ctx, &pkt);
					if (ret < 0)
					{
						av_log(NULL, AV_LOG_ERROR, "av_bsf_receive_packet error!\n");
						continue;
					}*/
					
					in_stream = ifmt_ctx->streams[pkt.stream_index];
					out_stream = octx[OutPutType::TYPE_RTMP]->streams[stream_index];

					if (pkt.stream_index == audioindex) {

						//Write PTS
						AVRational time_base1 = in_stream->time_base;
						//Duration between 2 frames (us)
						AVRational r_framerate1 = { in_stream->codecpar->sample_rate, 1 };
						int64_t calc_duration = (double)AV_TIME_BASE *(1 / av_q2d(r_framerate1));

						//Parameters
						pkt.pts = av_rescale_q(nb_samples*calc_duration, time_base_q, time_base1);
						pkt.dts = pkt.pts;
						pkt.duration = pkt.size;

						nb_samples += pkt.size;
						cur_pts_a = nb_samples * calc_duration;

						int64_t pts_time = av_rescale_q(pkt.dts, time_base1, time_base_q);
						int64_t now_time = av_gettime() - start_time;
						if (pts_time > now_time)
						{
							av_usleep(pts_time - now_time);
							//printf("音视频推流 音频睡眠一段时间\n");
						}
					}
					pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
					pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
					pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
					pkt.pos = -1;
					pkt.stream_index = stream_index;

					printf("Write 1 Packet. size:%5d\tpts:%lld\n", pkt.size, pkt.pts);
					//Write
					ret = av_write_frame(octx[OutPutType::TYPE_RTMP], &pkt);
					if (ret < 0) {
						av_log(NULL, AV_LOG_ERROR, "(音视频)rtmp发送数据包出错err code = %d\n", ret);
						rtmp_logger->error("(音视频)rtmp发送数据包出错err code = {}!", ret);
						av_packet_unref(&pkt);
						break;
					}

				}
				else {
					audio_end = 0;
					continue;
				}
			}
			

			total_pts = pkt.pts;
			//释放	
			av_packet_unref(&pkt);
		}
	

av_compare_ts(比较时间戳,决定写入视频还是写入音频)这个函数不常用,大家可以看下ffmpeg的接口熟悉下。这个同步算法也有一点借鉴了雷神的帖子最简单的基于FFmpeg的封装格式处理:视音频复用器(muxer)但这个是基于文件的,我们处理的是数据流,所以不太适合,大家了解就好。

经过多次测试发现,终端视频数据有的可以解出fps帧率,也就是r_frame_rate参数

AVRational r_framerate1 = ifmt_ctx->streams[videoindex]->r_frame_rate;  

有的却不能解出,可以解出当然就可以用帧率控制视频的推流速度了,无法解出的使用帧和帧的时间差来计算,固定推25帧,如果帧和帧的时间差是40ms,这一帧就推一次,如果是80ms这一帧就推两次,如果是120ms,这一帧就推三次,此方法为补帧。

int f_seek = 1;
int h264_fps = av_q2d(ictx_v->streams[videoindex]->r_frame_rate);
if (h264_fps < 25)
{
	f_seek = 1;
}
else
{
	double f = (double)this->time_delay_v / 40;
	f_seek = round(f);
	if (f_seek == 0)
	{
			f_seek = 1;
	}
}

首先判断能否从h264裸流里读出帧率,如果帧率小于25,说明读出来了,那就通过帧率来控制推流速度就可以,f_seek赋值为1。

f_seek 为要这一帧要推送几遍,time_delay_v是通过两帧的时间戳计算出来的差值,也就是两帧相差的时间。因为有的时候两帧并不是相差40的整数倍,所以这里要使用四舍五入,比如如果如果两帧相差70ms。

 之前我们使用了alaw / mulaw格式来打开音频输入,这里有个问题,这个格式里的采样率44100hz,比特率是325kbit/s,所以在同步音频的时候,

AVRational r_framerate1 = { in_stream->codecpar->sample_rate, 1 };

 int64_t calc_duration = (double)AV_TIME_BASE *(1 / av_q2d(r_framerate1));

这两行代码计算出来的是不准的,所以我们要手动修改音频输入端的参数来使音频时间戳计算准确。

ictx_a->bit_rate = 64000;
ictx_a->streams[0]->codecpar->bit_rate = 64000;
ictx_a->streams[0]->codecpar->sample_rate = 8000;
ictx_a->streams[0]->time_base.den = 8000;

以上是处理g711a和g711u的音频编码格式。而g726和adpcma我们却不能直接用。 会报错误:audio codec adpcm_g762 not compatible flv.   因为flv不支持这种格式。思来想去,我的处理办法是接收到g726格式的音频数据,先解码成pcm再编码成g711a,之后的处理就个g711a一样了,adpcma相同。首先要初始化解码器

AVCodec *g726_codec = avcodec_find_decoder(AV_CODEC_ID_ADPCM_G726LE);
	if (!g726_codec)
	{
		av_log(NULL, AV_LOG_ERROR, " AV_CODEC_ID_ADPCM_G726LE avcodec_find_decoder error\n!\n");
		return -1;
	}
	g726_dec_ctx = avcodec_alloc_context3(g726_codec);
	g726_dec_ctx->bits_per_coded_sample = g726_decode_type;
	g726_dec_ctx->channels = 1;
	g726_dec_ctx->sample_fmt = AV_SAMPLE_FMT_S16;
	g726_dec_ctx->sample_rate = 8000;
	g726_dec_ctx->codec_type = AVMEDIA_TYPE_AUDIO;

	if ((ret = avcodec_open2(g726_dec_ctx, g726_codec, NULL)) < 0) {
		av_log(NULL, AV_LOG_ERROR, "Could not open g726_dec_ctx!\n");
		return ret;
	}

 解码方式的ID有AV_CODEC_ID_ADPCM_G726LE合AV_CODEC_ID_ADPCM_G726,这跟使用的海思芯片有关,ffmpeg库解码海思G726库编码音频数据,一下是我了解的音频相关知识:

音频数据在音频间隔40ms,每帧320字节的设备里改变帧率,也不会改变音频的采样时间和采样率。有的设备是g726编码, 假如pcm的采样率为8000,采样位宽为16bit,通道数为1,20ms采样数据大小为320字节  采样率*位宽*通道数/1000毫秒* 20毫秒)/ 8 ,g711把pcm压缩一半,压缩后g711包大小为160字节 ,采样位宽8bit,20ms间隔。而冀A0J6D5 设备如果设置音频格式为g726 40k,发来的数据编码参数为8(g726),数据体为160字节大小,采样间隔时间是20ms。

G.726编解码器把128kbit/s线性数据(64kbit/s PCM数据)压缩为16kbit/s24kbit/s32kbit/s40kbit/s数据压缩比分别为8:116:34:116:5,码字分别为2345bits。采用越高压缩比,码率越小,质量越差。

如果发来的是40k,那压缩比为16:5 = 3.2, 320字节pcm压缩成g726应该是100字节,而160字节的g726解压缩后的pcm应该是512字节, 这两种数据无论是哪一种均和网上所说的资料不对应。光从数据大小的角度看已经不正确了。还有一种g726编码格式的音频,数据体为164字节,这说明前4字节为海思私有头0x00,0x01,0x50,0x00。实际数据160字节,需要在解码时处理掉前4字节。采样间隔是40ms,如果是pcm采样的话数据体应该是640自己,640/160=4 说明压缩比为4:1,也就是压缩成了32kbit/s,这是正确的。

bits_per_coded_sample参数设置音频的压缩比,所以在计算的时候我们要通过数据包的大小除以帧和帧的时间间隔,算出压缩比。

switch (数据体大小 / 采样间隔时间)  
{  
case 2:  
    g726_decode_type = G726_16KBPS;  
    break;  
case 3:  
    g726_decode_type = G726_24KBPS;  
    break;  
case 4:  
    g726_decode_type = G726_32KBPS;  
    break;  
case 5:  
    g726_decode_type = G726_40KBPS;  
    break;  
default:  
    g726_decode_type = G726_40KBPS;  
}  

网上还有一种办法,是把所有音频全部转换成pcm再转成aac,但用在我的代码结构里不行,因为一帧g711可以解码成一帧pcm,多个pcm才能编码成一帧aac数据,编码后的时间戳无法计算。导致音视频不能同步。Car-eye JT/T1078 视频服务器开发过程中的音频处理

部标jt1078实时音视频,音视频同步推流_第3张图片

随着对ffmpeg认识不断深入,我也在思考,是否可以脱离avformat_open_input,直接把队列的数据包赋值个avpacket,再做相应的处理,之后会再研究此方法,等待后续更新。

 我只是写出了我的实现思路,如果哪位大神有更好的解决方法,还请指点小弟一二。不胜感激。小弟也是才步入音视频领域的小白,请看完文章的大神轻喷

 

 

你可能感兴趣的:(windows)