• 终端接收FFMEPG推送的流出现音频卡顿问题


    TOC

    1. 终端音频卡顿的可能情况

    分析问题有一个很有用的链路分析法,将链路切分为多个环节,分析每个环节从而找到问题根源。

    解码框图

    接收码流数据 -> 解复用 -> 音视频解码 -> 音视频同步 -> 音视频(显卡、声卡)输出

    音频输出环节分析:

    音频卡顿是人感官听到的,也就是声卡发出的声音有卡顿;
    声卡卡顿的可能原因:1. 声卡硬件异常 2.声卡收到的音频有问题

    1. 声卡硬件异常的问题,只能换硬件。
    2. 声卡的上一环节送过来的声音有卡顿。

    音视频同步环节分析:

    1. 音频解码后的数据出现音频卡顿
    2. 解码后的音频数据送给sample buffer出现了溢出。

    音频解码器环节分析:

    音频解码器工作比较简单,收到数据就进行解码,解码后就将数据发送给下一级。

    1. 编码的原始数据就是音频卡顿。
    2. 接收解复用的数据时候,发送过来的数据量抖动太大,audio buffer 出现溢出。
    3. 音频的PTS小于音频包到达的时间,大部分解码器都会直接扔掉。

    解复用环节分析:

    这一环节一般不会出问题。

    分析总结:

    分析了整个链路的每个环节,音频卡顿的几种可能:

    1. 音频来得太迟(PTS < 到达时间),音频包被丢弃了
    2. 接收到的流码率抖动太大,解码端的相关音频buffer出现溢出
    3. 原始音频数据就是卡顿。
    4. 声卡硬件问题

    情况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;
    }
  • 相关阅读:
    一文看懂Fluentd语法
    mongo 使用聚合合并字段
    加速开发流程的 Dockerfile 最佳实践
    nodejs之RSA加密/签名
    nodejs之https双向认证
    自签证书生成
    白话理解https
    一文看懂k8s Deployment yaml
    基于xtermjs实现的web terminal
    intelliJ 中文设置
  • 原文地址:https://www.cnblogs.com/standardzero/p/12557666.html
Copyright © 2020-2023  润新知