FFMPEG推送UDP码流存在的问题分析与解决方案

一、问题描述

ffmpeg-3.1.1开始,为了控制udp码流发送稳定性,加入了bitrate参数。bitrate参数在ffmpeg官网释义如下:

bitrate=bitrate

If 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要求的速率,因此测试结果显示,改进之后的码率波动比之前更小。



评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值