一、问题描述
ffmpeg-3.1.1开始,为了控制udp码流发送稳定性,加入了bitrate参数。bitrate参数在ffmpeg官网释义如下:
bitrate=bitrateIf set to nonzero, the output will have the specified constant bitrate if the input has enough packets to sustain it.
其实现方式是,根据UDP包大小与用户设置的bitrate值,规划每个UDP包发送的时刻,从而实现数据的均匀发送,达到平稳码流的目的。FFMPEG中原始实现代码如下: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;
int64_t burst_interval = s->bitrate ? (s->burst_bits * 1000000 / s->bitrate) : 0; //发送间隔
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); //获取UDPContext的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); //从fifo中读取长度为len的数据
pthread_mutex_unlock(&s->mutex);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_cancelstate);
//bitrate控制发送的代码
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); //等待delay长度的时间
} else { //当前时间到达或超过目标时间,则直接发送数据
if (timestamp - burst_interval > target_timestamp) {
start_timestamp = timestamp - burst_interval; //当前时间超过目标时间的差值已经大于发送间隔的情况下,重新规划start_timestamp
sent_bits = 0; //发送比特数清零
}
}
sent_bits += len * 8; //计算发送比特数
target_timestamp = start_timestamp + sent_bits * 1000000 / s->bitrate; //根据起始时间和发送的比特数以及bitrate值来更新目标时间
}
//下面是UDP发送
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;
}
以mpegts over udp为例,将该参数设置为与封装码率muxrate一致的值,输出的udp码率与不加bitrate的情况相比,在平稳性上确实有了很大的提升,但同时也产生了一个问题,那就是当封装层输出的码率过大,或者封装层输出的码率波动过大,或者即便封装层码率平稳,但是转码或推流较长时间后,都会出现FIFO溢出,从而导致转码失败。
二、原因分析
出现以上情况,究其原因,都是以为发送速率低于了封装层的输出速率,导致fifo中累积过多数据所致。
具体情况分析如下:
进入UDP发送线程的时刻是随机的,可能在规划的target_timestamp前,也可能在规划的target_timestamp之后。若在target_timestamp之前,则计算当前时间与target_timestamp之间的差值,记为delay,并睡眠长度为delay的时间,然后再发送;若在target_timestamp之后,那么立刻发送数据包,并根据当前发送的时间重新计算下一次的发送时间target_timestamp。那么,发送延后并重新规划target_timestamp,会导致实际单位时间内发送的数据量小于设定的bitrate,因此当封装模块以恒定的muxrate往FIFO中写数据,而实际从FIFO中取出并发送的速率小于设定的bitrate,也就是小于muxrate的情况下,会导致FIFO中的数据越积越多,最后溢出。
为了更好理解以上所述,简单地绘制了一个示意图,如下:
三、解决方案
为了使实际发送的数据量跟设定的bitrate相等,我们需要计算总的延时sum_time_delayed,以及延时总量包含的数据包的个数nb_udp_delayed,在每次进入发送线程时,如果当前时间timestamp早于规划的target_timestamp,则判断是否有被delay的数据包,如果有,则直接发送,并将nb_udp_delayed自减1;否则仍然sleep(target_timestamp- timestamp)长度的时间,然后到target_timestamp时醒来并发送。相当于如果判断到有被延时的包,则在两次发送间隔中插入一次发送,以保证实际的发送数据量与设定的bitrate相一致。
另外,为了防止FIFO溢出,需要对FIFO状态进行监测,当FIFO中数据量达到FIFO总大小的一定比例时(例如我在实现中设置为90%),则在当前时间早于target_timestamp时,也直接插入一次发送。
四、结果验证
结果验证包括两个方面,一是输出码率与设定的bitrate值保持一致,二是不再发生FIFO溢出。
(1) 为了验证第一个问题,打印出FIFO中数据的占比,转码几个小时,FIFO中的数据比例一直恒定在一个值(上下1%的波动),例如79%,说明发送数据的速率与muxrate保持一致。而未加补偿之前的原始程序在转码过程中,FIFO中的数据比例是在不断上升的,比如两个小时内从60%一直上升到90%以上,说明实际发送速率比muxrate(bitrate =muxrate)要低。
(2) 对于第二个问题,转码超过72小时,未再出现fifo overrun。
五、意外收获:
由于加入了发送补偿,使得实际发送的速率更接近于bitrate要求的速率,因此测试结果显示,改进之后的码率波动比之前更小。