终端接收FFMEPG推送的流出现音频卡顿问题
1. 终端音频卡顿的可能情况
分析问题有一个很有用的链路分析法,将链路切分为多个环节,分析每个环节从而找到问题根源。
解码框图
接收码流数据 -> 解复用 -> 音视频解码 -> 音视频同步 -> 音视频(显卡、声卡)输出
音频输出环节分析:
音频卡顿是人感官听到的,也就是声卡发出的声音有卡顿;
声卡卡顿的可能原因:1. 声卡硬件异常 2.声卡收到的音频有问题
- 声卡硬件异常的问题,只能换硬件。
- 声卡的上一环节送过来的声音有卡顿。
音视频同步环节分析:
- 音频解码后的数据出现音频卡顿
- 解码后的音频数据送给sample buffer出现了溢出。
音频解码器环节分析:
音频解码器工作比较简单,收到数据就进行解码,解码后就将数据发送给下一级。
- 编码的原始数据就是音频卡顿。
- 接收解复用的数据时候,发送过来的数据量抖动太大,audio buffer 出现溢出。
- 音频的PTS小于音频包到达的时间,大部分解码器都会直接扔掉。
解复用环节分析:
这一环节一般不会出问题。
分析总结:
分析了整个链路的每个环节,音频卡顿的几种可能:
- 音频来得太迟(PTS < 到达时间),音频包被丢弃了
- 接收到的流码率抖动太大,解码端的相关音频buffer出现溢出
- 原始音频数据就是卡顿。
- 声卡硬件问题
情况1和情况2都可以使用相关工具分析判断;
情况3需要录流,软件解码出音频PCM,播放PCM可以知道;
情况4用不同的终端对比测试可以判断。
2. 解决方案
2.1 音频PTS错误问题
max_delay
: 这个参数可以拉开PCR和PTS直接的差距,从而解决音频包的PTS小于音频包到达时间;ffmpeg 默认是0.7s, 可以根据需要设置它的值。
设置max_delay
参数的接口:
av_dict_set(&dicCtx, "max_delay", "1000000", 0)
2.2 输出码率抖动问题
核心问题就是让FFMPEG码流平滑输出
ffmpeg 命令行输出平滑码率:
ffmpeg -re -i input.ts -c copy -muxrate 7000000 -f mpegts "udp://227.40.50.60:1234?pkt_size=1316&fifo_size=27887&bitrate=7000000&burst_bits=1000000"
说明:
input.ts:输入流
muxrate
:设置复用码率,ffmpeg通过填充空包达到CBR流,这个值不能低于input.ts流的实际码率。
fifo_size
:ffmpeg采用av_fifo来缓存一定量数据用于控制平滑输出,从ffmpeg的源码中,这里设置27887,实际上ffmpeg内部分配大小为27887*188bytes的,可以
在libavformat/udp.c
中查看。
bitrate
:控制码率平滑输出,需要设置和muxrate参数一样,否则从av_fifo中输出和输入对应不上导致av_fifo会出现上下溢情况。
burst_bits
:设置突发码率的,这里设置为1000000突发码率,控制平滑输出时,防止输入流那边有突发,需要设置一个突发阀值。
音视频+表+空包的码率(muxrate)是恒定的,ffmpeg 通过调整空包码率来保持复用码率(CBR码率)恒定;
但是这个恒定的码率是秒级别的,直接输出的话,码率还是会抖动,bitrate参数是控制平滑输出的;
若码率6Mbit/s,一秒内要输出的数据量就是5Mbit,平滑处理的时,将6Mbit的数据,分成10等分(举例说明),每隔0.1s就发送500Kbit的数据,达到平滑输出的目的。
FFMPEG 平滑输出的核心代码:
ffmpeg处理UDP平滑输出代码再libavformat/udp.c中的circular_buffer_task_tx接口中,如下代码:
static void *circular_buffer_task_tx( void *_URLContext)
{
URLContext *h = _URLContext;
UDPContext *s = h->priv_data;
int old_cancelstate;
int64_t target_timestamp = av_gettime_relative();
int64_t start_timestamp = av_gettime_relative();
int64_t sent_bits = 0;
//根据上面设置的burst_bits计算出码率突发值,单位为us
int64_t burst_interval = s->bitrate ? (s->burst_bits * 1000000 / s->bitrate) : 0;
//max_delay根据平滑码率bitrate值及每一次发送UDP包的大小来设置的,单位为us,一般UDP发送
大小设置 为1316字节,即h->max_packet_size值为1316,确定好max_delay后用于下面控制码率
平滑输 出。
int64_t max_delay = s->bitrate ? ((int64_t)h->max_packet_size * 8 * 1000000 / s->bitrate + 1) : 0;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancelstate);
pthread_mutex_lock(&s->mutex);
if (ff_socket_nonblock(s->udp_fd, 0) < 0) {
av_log(h, AV_LOG_ERROR, "Failed to set blocking mode");
s->circular_buffer_error = AVERROR(EIO);
goto end;
}
for(;;) {
int len;
const uint8_t *p;
uint8_t tmp[4];
int64_t timestamp;
len=av_fifo_size(s->fifo);
while (len<4) {
if (s->close_req)
goto end;
if (pthread_cond_wait(&s->cond, &s->mutex) < 0) {
goto end;
}
len=av_fifo_size(s->fifo);
}
av_fifo_generic_read(s->fifo, tmp, 4, NULL);
len=AV_RL32(tmp);
av_assert0(len >= 0);
av_assert0(len <= sizeof(s->tmp));
av_fifo_generic_read(s->fifo, s->tmp, len, NULL);
pthread_mutex_unlock(&s->mutex);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_cancelstate);
//控制码率平滑输出
if (s->bitrate) {
timestamp = av_gettime_relative();
if (timestamp < target_timestamp) {
int64_t delay = target_timestamp - timestamp;
if (delay > max_delay) {
delay = max_delay;
start_timestamp = timestamp + delay;
sent_bits = 0;
}
av_usleep(delay);
} else {
if (timestamp - burst_interval > target_timestamp) {
start_timestamp = timestamp - burst_interval;
sent_bits = 0;
}
}
sent_bits += len * 8;
target_timestamp = start_timestamp + sent_bits * 1000000 / s->bitrate;
}
p = s->tmp;
while (len) {
int ret;
av_assert0(len > 0);
if (!s->is_connected) {
ret = sendto (s->udp_fd, p, len, 0,
(struct sockaddr *) &s->dest_addr,
s->dest_addr_len);
} else
ret = send(s->udp_fd, p, len, 0);
if (ret >= 0) {
len -= ret;
p += ret;
} else {
ret = ff_neterrno();
if (ret != AVERROR(EAGAIN) && ret != AVERROR(EINTR)) {
pthread_mutex_lock(&s->mutex);
s->circular_buffer_error = ret;
pthread_mutex_unlock(&s->mutex);
return NULL;
}
}
}
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancelstate);
pthread_mutex_lock(&s->mutex);
}
end:
pthread_mutex_unlock(&s->mutex);
return NULL;
}