上篇讲了视频推流部分,这一篇主要针对1078协议讲音频部分和最后音视频同步处理。
根据jt1078里表12看到,音频编码还是比较多的,我到现在遇到的终端下发的音频数据编码格式有4种,G711A,G711U,G726,ADPCMA。
音频也需要有一个输入,跟视频一样,要调用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帧 ......我们在同步的时候要把时间戳同步好,
核心代码如下:
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/s、24kbit/s、32kbit/s、40kbit/s数据压缩比分别为8:1、16:3、4:1和16:5,码字分别为2、3、4和5bits。采用越高压缩比,码率越小,质量越差。
如果发来的是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 视频服务器开发过程中的音频处理
随着对ffmpeg认识不断深入,我也在思考,是否可以脱离avformat_open_input,直接把队列的数据包赋值个avpacket,再做相应的处理,之后会再研究此方法,等待后续更新。
我只是写出了我的实现思路,如果哪位大神有更好的解决方法,还请指点小弟一二。不胜感激。小弟也是才步入音视频领域的小白,请看完文章的大神轻喷