1 背景介绍

若仅仅发送音频数据,不需要PACER模块:

  1. 一帧音频数据本身不大,不会超过以太网的最大报文长度。一个RTP报文可以搞定,按照打包时长的节奏发送就可以。但视频数据不能按照音频数据的思路发送,一帧视频可能很大,远大于以太网的1500byte,需要分别封装在几个RTP报文中,若这些视频帧RTP报文一起发送到网络上,必然会导致网络瞬间拥塞,产生丢包、抖动等异常。
  2. 大多数编解码格式下,一帧音频数据长度固定,音频码率持续平稳,码率不会出现忽高忽低现象。但是一帧视频数据长度受内容影响严重,I、P、B帧间的长度相差非常大,直接发送会使网络波动幅度很大。尤其是在WIFI环境下,受限WIFI的调度机制,媒体数据能否平稳发送,对弱网的WiFi环境下的通话质量影响很大。

PACER的目的就是让视频数据按照评估码率均匀地在各个时间片发送出去。

2 实现原理

2.1 设计PACER模块主要解决三个问题
2.1.1 怎么发

音频、视频、NACK、FEC、PADDING报文都要统一从PACER模块发送。若不区分报文优先级,势必会对系统延时产生很大的影响。

所以音视频数据编码和RTP切分打包后,首先将RTP报文存在pace queue队列,并将报文元数据(packet id、size、timestamp、重传标示)送到pacer queue进行排队等待发送,插入队列的元数据会进行优先级排序。

pace queue是一个基于优先级排序的多维链表,它并不是一个先进先出的FIFO,而是一个按优先级排序的list。报文优先级规则是:

  • 优先级高的报文排在FIFO的前面,低的排在后面。
  • 首先判断报文的priority等级,等级越小的优先级越高(priority等级根据报文类型进行分类)。
  • 然后判断重发标示,重发的报文比普通报文的优先级更高。
  • 最后是判断视频帧timestamp,越早的视频帧优先级越高。

pacer每次触发发送事件时是先从queue的最前面取出优先级最高的报文进行发送,这样做的目的是让视频在传输的过程中延迟尽量小,重传的报文尽快能到达防止等待卡顿。pace queue还可以设置最大延迟,如果超过最大延迟,会计算queue中数据发送所需要的码率,并且会把这个码率替代target bitrate作为budget参考码率来加速发送(最大延时详细处理流程在2.2小节有介绍)。

根据报文类型确定数据优先级处理函数如下:

/**
 * @brief 根据RTP报文类型获取对应的优先级
 * @details 优先级数值越小,优先级越高,用于Pacer队列中报文的排序,确保高优先级报文优先发送
 * @param type RTP报文类型,枚举值,包含音频、重传、前向纠错、视频、填充等类型
 * @return int 优先级数值,kFirstPriority为基础值,不同类型报文在此基础上累加得到最终优先级
 */
int GetPriorityForType(RtpPacketMediaType type) {
    // Lower number takes priority over higher.(数值越小,优先级越高)
    switch (type) {
        case RtpPacketMediaType::kAudio: 
            // Audio is always prioritized over other packet types.(音频始终比其他类型报文优先级高)
            return kFirstPriority + 1;
        case RtpPacketMediaType::kRetransmission: 
            // Send retransmissions before new media.(重传报文比新的媒体报文优先级高)
        case RtpPacketMediaType::kForwardErrorCorrection: 
            // 前向纠错报文,与重传报文优先级同级,用于保障数据传输可靠性
        case RtpPacketMediaType::kVideo: 
            // Video has "normal" priority, in the old speak.(视频报文为“正常”优先级)
            return kFirstPriority + 2;
        case RtpPacketMediaType::kRedundancy:
            // Send redundancy concurrently to video. If it is delayed it might have a lower chance of being useful.
            // 冗余报文与视频报文并行发送,若延迟则可用性降低,故优先级低于视频
            return kFirstPriority + 3;
        case RtpPacketMediaType::kPadding: 
            // Packets that are in themselves likely useless, only sent to keep the BWE high.
            // 填充报文本身无实际数据意义,仅用于维持带宽估计(BWE)值,优先级最低
            return kFirstPriority + 4;
        default:
            // 默认情况,赋予较低优先级,避免未定义类型报文占用高优先级资源
            return kFirstPriority + 5;
    }
}

按照优先级弹出(POP)数据处理函数如下:

/**
 * @brief 重载小于运算符,用于RoundRobinPacketQueue队列中报文的优先级比较
 * @details 当队列进行排序或弹出操作时,通过此函数判断两个报文的优先级高低,优先级高的报文优先被处理
 * @param other 待比较的另一个QueuedPacket对象
 * @return bool 若当前报文优先级低于other,则返回true;否则返回false(用于排序时将高优先级报文排在前面)
 */
bool RoundRobinPacketQueue::QueuedPacket::operator<(const RoundRobinPacketQueue::QueuedPacket& other) const {
    // 第一步:先比较优先级数值,数值小的优先级高
    if (priority_ != other.priority_) {
        // 若当前优先级数值大于other,说明当前优先级低,返回true(排序时当前报文会排在后面)
        return priority_ > other.priority_;
    }
    // 第二步:优先级相同时,比较是否为重传报文,重传报文优先级更高
    if (is_retransmission_ != other.is_retransmission_) {
        // other.is_retransmission_为true时,返回true,说明当前报文优先级低于other(重传报文)
        return other.is_retransmission_;
    }
    // 第三步:优先级和重传属性都相同时,按入队顺序比较,早入队的报文优先处理
    // enqueue_order_为入队时的序号,序号小的早入队
    return enqueue_order_ > other.enqueue_order_;
}
2.1.2 什么时候发

PacingController::NextSendTime、PacingController::ProcessPackets是PACER模块两个核心函数,PacingController::ProcessPackets按照PacingController::NextSendTime控制的节奏周期调用,完成PACER平滑发送功能。

PacingController::NextSendTime在控制发送节奏上,有两种模式kPeriodic、kDynamic。kDynamic模式暂未完全解析,此处先记录kPeriodic实现方式。

/**
 * @brief 计算下一次发送报文的时间,控制PACER模块的发送节奏
 * @details 根据当前模块状态(暂停、探测、工作模式)确定下一次触发发送任务的时间点
 * @return Timestamp 下一次发送报文的时间戳,若暂停则按固定间隔返回,若探测则优先返回探测时间
 */
Timestamp PacingController::NextSendTime() const {
    // 获取当前系统时间戳
    Timestamp now = CurrentTime();
    
    // 若PACER模块处于暂停状态,按固定暂停间隔(kPausedProcessInterval)返回下一次发送时间
    if (paused_) {
        return last_send_time_ + kPausedProcessInterval;
    }
    
    // 若处于带宽探测状态(probing),探测任务优先,返回下一次探测的时间点
    if (prober_.is_probing()) {
        // If probing is active, that always takes priority.(探测激活时,始终优先处理探测任务)
        Timestamp probe_time = prober_.NextProbeTime(now);
        // |probe_time|== PlusInfinity indicates no probe scheduled.(probe_time为无穷大表示无探测任务)
        if (probe_time != Timestamp::PlusInfinity() && !probing_send_failure_) {
            return probe_time;
        }
    }
    
    // 处理周期性(kPeriodic)模式,按固定时间间隔触发发送任务
    if (mode_ == ProcessMode::kPeriodic) {
        // In periodic non-probing mode, we just have a fixed interval.
        // 非探测的周期性模式下,每次发送间隔为min_packet_interval(通常配置为5ms)
        return last_process_time_ + min_packet_interval;
    }
    
    // 若为动态模式(kDynamic)或其他未定义模式,默认返回当前时间(后续需补充kDynamic模式逻辑)
    return now;
}

kPeriodic模式下,固定每隔5ms调用一次发送报文任务。

2.1.3 发多少

PacingController::ProcessPackets被定时触发后,会计算当前时间和上次被调用时间的时间差,然后将时间差参数传入media_budget_,media_budget_算出当前时间片网络可以发送多少数据,然后从pacer queue当中取出报文元数据进行网络发送。

media_budget_根据评估出来的参考码率计算这次定时事件能发送多少字节的公式如下:
remain_bytes = delta_time × target_bitrate ÷ 8

  • delta_time:上次检查时间点和这次检查时间点的时间差。
  • target_bitrate:pacer的参考码率,是由estimator(带宽估计器)根据网络状态评估出来的。
  • remain_bytes:每次触发发包时会减去发送报文的长度size,如果remain_bytes > 0,继续从pace queue中取下一个报文进行发送,直到remain_bytes <= 0或者pace queue没有更多的报文。

如果pacer queue没有更多待发送的报文,但media_budget_计算出还可以发送更多的数据,这个时候pacer会进行padding(填充)报文补充。

2.2 PACER模块引入延时问题规避方法
2.2.1 max_pacing_delay

PACER模块定量计算发送网络报文数据量,相当于缓存等待发送,必然会引起延迟。为了保证实时性,PACER模块有个max_pacing_delay全局变量,配置最大缓冲发送延时时间上限,若最大缓冲延时大于该值,就要重新调整PACER模块的目标码率,保证当前数据都能及时发送出去。

max_pacing_delay生效流程如下:

/**
 * @brief PacingConfig类的构造函数,初始化PACER模块的关键配置参数
 * @details 从字段试验(field trial)中解析 pacing_factor( pacing系数)和 max_pacing_delay(最大 pacing延迟)
 * @param field_trial 字段试验名称,用于获取试验配置,默认为"WebRTC-Video-Pacing"
 */
PacingConfig::PacingConfig(const std::string& field_trial) {
    // 初始化pacing_factor,默认值为PacedSender::kDefaultPaceMultiplier(通常为1.0),用于调整发送速率的倍数
    pacing_factor("factor", PacedSender::kDefaultPaceMultiplier),
    // 初始化max_pacing_delay,默认值为PacedSender::kMaxQueueLengthMs(通常为100ms),即最大缓冲发送延迟
    max_pacing_delay("max_delay", TimeDelta::Millis(PacedSender::kMaxQueueLengthMs));
    
    // 从指定的字段试验中解析配置,若试验存在则覆盖默认值
    ParseFieldTrial({&pacing_factor, &max_pacing_delay}, field_trial::FindFullName("WebRTC-Video-Pacing"));
}

/**
 * @brief 在VideoSendStreamImpl类中配置PACER的队列时间限制(即max_pacing_delay)
 * @details 将PacingConfig中初始化的max_pacing_delay配置到传输层(transport),确保传输层遵循该延迟限制
 * @param transport 传输层对象,负责实际的数据发送
 * @param config PacingConfig配置对象,包含max_pacing_delay等关键参数
 */
VideoSendStreamImpl::VideoSendStreamImpl(/* 其他参数 */, const PacingConfig& config) {
    // 将max_pacing_delay设置到transport的队列时间限制中,控制数据在传输队列中的最大停留时间
    transport->SetQueueTimeLimit(config.max_pacing_delay);
}

/**
 * @brief 传输层(如PacingController)设置队列时间限制的实现函数
 * @details 保存外部传入的max_pacing_delay,用于后续ProcessPackets函数中判断是否需要调整发送码率
 * @param limit 队列时间限制,即max_pacing_delay的值
 */
void PacingController::SetQueueTimeLimit(TimeDelta limit) {
    queue_time_limit_ = limit;
}

/**
 * @brief PACER模块核心处理函数,定时触发发送任务,并根据队列延迟调整目标码率
 * @details 计算当前时间与上次处理时间的差值,更新发送预算,若队列延迟超过限制则提高目标码率以加速发送
 * @param now 当前系统时间戳
 */
void PacingController::ProcessPackets(Timestamp now) {
    // 计算上次处理到当前的时间差(delta_time)
    TimeDelta elapsed_time = now - last_process_time_;
    if (elapsed_time > TimeDelta::Zero()) {
        // 初始目标码率为pacing_bitrate_(由带宽估计器提供的基础码率)
        DataRate target_rate = pacing_bitrate_;
        // 获取当前队列中待发送数据的总大小
        DataSize queue_size_data = packet_queue_.Size();
        
        if (queue_size_data > DataSize::Zero()) {
            // 更新队列的平均等待时间(AverageQueueTime),反映数据在队列中的停留情况
            packet_queue_.UpdateQueueTime(now);
            // 计算剩余的平均时间:队列时间限制 - 队列平均等待时间,确保数据能在限制内发送完成
            TimeDelta avg_time_left = std::max(TimeDelta::Millis(1), queue_time_limit_ - packet_queue_.AverageQueueTime());
            // drain_large_queues_为true时,表示需要加速发送以排空大队列,避免延迟超标
            if (drain_large_queues_) {
                // 计算排空当前队列所需的最小码率:队列数据量 / 剩余平均时间
                DataRate min_rate_needed = queue_size_data / avg_time_left;
                // 若最小需求码率大于当前目标码率,则更新目标码率为最小需求码率,加速发送
                if (min_rate_needed > target_rate) {
                    target_rate = min_rate_needed;
                    RTC_LOG(LS_VERBOSE) << "bwe: large_pacing_queue pacing_rate_kbps=" << target_rate.kbps();
                }
            }
        }
        
        // 根据处理模式(kPeriodic/kDynamic)更新发送预算
        if (mode_ == ProcessMode::kPeriodic) {
            // In periodic processing mode, the IntervalBudget allows positive budget
            // up to (process interval duration) * (target rate), so we only need to
            // update it once before the packet sending loop.
            // 周期性模式下,IntervalBudget的正预算上限为“处理间隔 × 目标码率”,只需在发送循环前更新一次
            media_budget_.set_target_rate_kbps(target_rate.kbps());
            media_budget_.UpdateBudget(elapsed_time);
        } else {
            // 动态模式(kDynamic)下,更新目标码率和发送预算(后续需补充完整逻辑)
            media_budget_.set_target_rate_kbps(target_rate.kbps());
            UpdateBudgetWithElapsedTime(elapsed_time);
        }
        // 保存当前目标码率,用于后续发送预算计算
        media_rate_ = target_rate;
    }
    
    // 后续逻辑:根据media_budget_的剩余预算(remain_bytes)从队列中取出报文发送,此处省略...
    last_process_time_ = now;
}

很明显这仅仅是一个迫不得已的规避方法,实际应用中,这种方法会出现码率梯度上升现象。

2.2.2 编码算法与码控模块配合

PACER模块实现不复杂,但是要想真正做好PACER功能,仅仅靠一个PACER模块是不够的,需要与视频编码器的码控模块配合:

  1. 首先探测模块配置码率给编码器,编码器一定要保证在可控周期内码率收敛到配置的码率参数值以内,否则会给PACER模块造成的累计延时越来越大的压力。
  2. 另外I帧码率也要在合理范围内。若I帧数据量超大,势必导致关键帧传输延时变大,影响端到端系统延时。

这些参数都要根据自己的实际应用场景进行调优。

3 pace备注说明

上述是按照PacedSender算法对pacer模块进行分析,实际上WebRTC有两套pace算法:TaskQueuePacedSender和PacedSender。它们在RtpTransportControllerSend::RtpTransportControllerSend函数中通过use_task_queue_pacer_配置选择。

/**
 * @brief RtpTransportControllerSend类的构造函数,初始化PACER算法(PacedSender/TaskQueuePacedSender)
 * @details 根据字段试验(WebRTC-TaskQueuePacer)的开关,选择使用传统PacedSender还是基于任务队列的TaskQueuePacedSender
 * @param clock 时钟对象,用于获取时间戳
 * @param event_log 事件日志对象,记录PACER相关事件
 * @param predictor_factory 网络状态预测器工厂,用于网络状态估计
 * @param controller_factory 网络控制器工厂,用于创建网络控制逻辑
 * @param bitrate_config 码率约束配置,限制发送码率的上下限
 * @param process_thread 传统处理线程,用于PacedSender的定时任务
 * @param task_queue_factory 任务队列工厂,用于TaskQueuePacedSender创建任务队列
 * @param trials 字段试验配置,用于判断是否启用TaskQueuePacer
 */
RtpTransportControllerSend::RtpTransportControllerSend(
    Clock* clock,
    webrtc::RtcEventLog* event_log,
    NetworkStatePredictorFactoryInterface* predictor_factory,
    NetworkControllerFactoryInterface* controller_factory,
    const BitrateConstraints& bitrate_config,
    std::unique_ptr<ProcessThread> process_thread,
    TaskQueueFactory* task_queue_factory,
    const WebRtcKeyValueConfig* trials) 
    : clock_(clock),
      event_log_(event_log),
      bitrate_configurator_(bitrate_config),
      pacer_started_(false),
      process_thread_(std::move(process_thread)),
      // 判断是否启用TaskQueuePacer:从trials中检查"WebRTC-TaskQueuePacer"字段试验是否开启
      use_task_queue_pacer_(IsEnabled(trials, "WebRTC-TaskQueuePacer")),
      // 若启用TaskQueuePacer,则process_thread_pacer_为nullptr;否则创建传统PacedSender
      process_thread_pacer_(use_task_queue_pacer_
          ? nullptr
          : new PacedSender(
                clock,
                &packet_router_,  // 负责路由RTP报文到正确的传输通道
                event_log,
                trials,
                process_thread_.get())),  // 传入传统处理线程,用于定时触发发送
      // 若启用TaskQueuePacer,则创建TaskQueuePacedSender;否则为nullptr
      task_queue_pacer_(use_task_queue_pacer_
          ? new TaskQueuePacedSender(
                clock,
                &packet_router_,
                event_log,
                trials,
                task_queue_factory,  // 传入任务队列工厂,创建独立任务队列
                /*hold_back_window = */ PacingController::kMinSleepTime)  // 最小等待窗口,控制发送间隔
          : nullptr) {
    // 其他初始化逻辑,此处省略...
}

4 视频渲染平滑与音视频同步

视频渲染时间的确定需要考虑三方面的因素:网络抖动、网络延时、音视频同步

网络抖动:视频帧在网络上传输,会受到网络抖动的影响,不能收到立刻播放,需要进行适当的平滑

网络延时:一些报文在网络传输中,会存在丢包重传和延时的情况。渲染时需要进行适当缓存,等待丢失被重传的报文或者正在路上传输的报文

音视频同步:音视频报文传送到接收端,也不能完全保证同时接收。需要做一些时间校准,保证音视频偏差不影响体验

所以在计算视频渲染时间的时候,会结合这三方面的参数,计算一个合理值。

为解决上述问题,WebRTC通过三大核心模块协同工作:

  1. TimestampExtrapolator:基于卡尔曼滤波预估视频帧接收时间,为渲染时序提供基础;
  2. JitterEstimator:计算网络抖动引起的缓存延时,避免因帧延迟或重传导致的卡顿;
  3. 音视频同步(AVSyn):通过NTP时间校准与延时动态调整,确保音视频渲染时间差与采集时间差一致。

视频渲染时间计算核心流程

视频渲染时间的确定需综合网络抖动、延时与音视频同步三大因素,核心逻辑封装在VCMTiming::RenderTimeInternal函数中,整体调用链路如下:

RtpDemuxer::OnRtpPacket 接收RTP报文

RtpVideoStreamReceiver2::OnRtpPacket

RtpVideoStreamReceiver2::ReceivePacket

RtpVideoStreamReceiver2::OnReceivedPayloadData

RtpVideoStreamReceiver2::OnInsertedPacket

RtpVideoStreamReceiver2::OnAssembledFrame 组帧

RtpVideoStreamReceiver2::OnCompleteFrames

VideoReceiveStream2::OnCompleteFrame

VideoStreamBufferController::InsertFrame 插入缓冲区

VideoStreamBufferController::MaybeScheduleFrameForRelease

VideoStreamBufferController::FrameReadyForDecode

VideoStreamBufferController::OnFrameReady

VCMTiming::RenderTime

VCMTiming::RenderTimeInternal 核心计算

核心函数:VCMTiming::RenderTimeInternal

该函数是渲染时间计算的入口,负责结合预估接收时间与缓存延时,输出最终渲染时间,并确保延时在合理范围内。

/**
 * @brief 计算视频帧的最终渲染时间
 * @param frame_timestamp 视频帧的RTP时间戳(90kHz)
 * @param now 当前本地系统时间
 * @return Timestamp 最终渲染时间(若开启低延迟模式则返回0,即立即渲染)
 */
Timestamp VCMTiming::RenderTimeInternal(uint32_t frame_timestamp, Timestamp now) const {
    // 低延迟模式:尽可能立即渲染,不额外增加缓冲
    if (UseLowLatencyRendering()) {
        return Timestamp::Zero();
    }

    // 1. 调用TimestampExtrapolator预估帧的完整接收时间
    // 注意:ExtrapolateLocalTime会修改对象的环绕状态,非const方法
    Timestamp estimated_complete_time = 
        ts_extrapolator_->ExtrapolateLocalTime(frame_timestamp).value_or(now);

    // 2. 确保实际缓存延时在[min_playout_delay_, max_playout_delay_]范围内
    // current_delay_为动态计算的基础延时,Clamped用于截断边界值
    TimeDelta actual_delay = current_delay_.Clamped(min_playout_delay_, max_playout_delay_);

    // 3. 最终渲染时间 = 预估接收时间 + 合理缓存延时
    return estimated_complete_time + actual_delay;
}
4.1、TimestampExtrapolator:接收时间预估(卡尔曼滤波)

仅通过发送端的时间戳差值与采样率,无法准确判断接收端的帧到达节奏(受网络抖动影响)。WebRTC引入卡尔曼滤波,通过实时记录帧的RTP时间戳与本地接收时间,动态调整预估参数,实现接收时间的精准预测。

卡尔曼滤波的优势:

  • 噪声抑制:过滤网络波动、设备性能差异导致的时间戳噪声;
  • 动态适应:根据历史数据与当前观测值调整参数,适应网络变化;
  • 平滑处理:避免个别异常帧导致的预估结果大幅波动。

时间戳记录(Update函数)

每当接收新帧时,调用TimestampExtrapolator::Update记录帧的RTP时间戳与本地接收时间,并初始化首帧参考点。

/**
 * @brief 记录新帧的时间戳与接收时间,更新卡尔曼滤波的观测值
 * @param now 本地接收时间
 * @param ts90khz 视频帧的RTP时间戳(90kHz)
 */
void TimestampExtrapolator::Update(Timestamp now, uint32_t ts90khz) {
    // 计算当前时间与首帧接收时间的偏移(单位:ms)
    TimeDelta offset = now - start_;
    double t_ms = offset.ms();

    // 解包RTP时间戳(处理32位环绕问题)
    int64_t unwrapped_ts90khz = unwrapper_.Unwrap(ts90khz);

    // 初始化首帧参考点:设置首帧的解包时间戳与初始参数w_[1]
    if (!first_unwrapped_timestamp_) {
        // 初始猜测首帧时间偏移,此时t_ms接近0,w_[1] ≈ -w_[0] * 0 = 0
        w_[1] = -w_[0] * t_ms;
        first_unwrapped_timestamp_ = unwrapped_ts90khz;
        return;
    }

    // 计算观测残差:发送端两帧间隔 - 接收端两帧间隔
    double residual = (static_cast<double>(unwrapped_ts90khz) - *first_unwrapped_timestamp_) 
                      - (w_[0] * t_ms - w_[1]);

    // 后续卡尔曼滤波参数更新(见3.2.2)
    KalmanUpdate(t_ms, residual);
}

卡尔曼滤波参数更新(KalmanUpdate)

通过观测值t_ms(时间偏移)与residual(残差),动态调整滤波参数w_[0](预估接收采样率)与w_[1](预估首帧时间戳偏移)。

/**
 * @brief 卡尔曼滤波参数更新核心逻辑
 * @param t_ms 时间偏移(当前接收时间 - 首帧接收时间,单位:ms)
 * @param residual 观测残差(发送端间隔 - 接收端间隔)
 */
void TimestampExtrapolator::KalmanUpdate(double t_ms, double residual) {
    constexpr double kLambda = 0.99; // 遗忘因子,控制历史数据权重

    // 1. 构造观测矩阵T = [t_ms, 1]
    // 2. 计算卡尔曼增益K = P*T / (λ + T'*P*T)
    double K[2];
    K[0] = p_[0][0] * t_ms + p_[0][1] * 1;
    K[1] = p_[1][0] * t_ms + p_[1][1] * 1;
    double TPT = kLambda + t_ms * K[0] + 1 * K[1]; // T'*P*T + λ
    K[0] /= TPT;
    K[1] /= TPT;

    // 3. 更新状态向量w = w + K*(观测值 - 预测值)
    w_[0] += K[0] * residual;
    w_[1] += K[1] * residual;

    // 4. 更新协方差矩阵P = (I - K*T) * P / λ
    double t00 = p_[0][0];
    double t01 = p_[0][1];
    p_[0][0] = (1 - K[0] * t_ms) * t00 - K[0] * p_[1][0];
    p_[0][1] = (1 - K[0] * t_ms) * t01 - K[0] * p_[1][1];
    p_[1][0] = p_[1][0] * (1 - K[1]) - K[1] * t_ms * t00;
    p_[1][1] = p_[1][1] * (1 - K[1]) - K[1] * t_ms * t01;

    // 归一化协方差矩阵(确保数值稳定性)
    p_[0][0] /= kLambda;
    p_[0][1] /= kLambda;
    p_[1][0] /= kLambda;
    p_[1][1] /= kLambda;
}

预估接收时间(ExtrapolateLocalTime)

根据当前滤波状态,输入帧的RTP时间戳,输出预估的本地接收时间。

/**
 * @brief 根据RTP时间戳预估帧的本地接收时间
 * @param timestamp90khz 视频帧的RTP时间戳(90kHz)
 * @return std::optional<Timestamp> 预估接收时间(若未初始化则返回空)
 */
std::optional<Timestamp> TimestampExtrapolator::ExtrapolateLocalTime(uint32_t timestamp90khz) const {
    // 未初始化首帧数据,无法预估
    if (!first_unwrapped_timestamp_) {
        return std::nullopt;
    }

    // 解包RTP时间戳(处理环绕)
    int64_t unwrapped_ts90khz = unwrapper_.PeekUnwrap(timestamp90khz);

    // 启动阶段(接收帧数量不足):默认使用首帧接收时间
    if (packet_count_ < kStartUpFilterDelayInPackets) {
        return start_;
    }

    // 采样率异常(接近0):使用前一帧时间 + 固定间隔(90kHz转ms)
    if (w_[0] < 1e-3) {
        TimeDelta diff = TimeDelta::Millis(
            (unwrapped_ts90khz - *prev_unwrapped_timestamp_) / kRtpTicksPerMs // kRtpTicksPerMs = 90
        );
        // 防止时间为负(环绕场景)
        if (prev_.us() + diff.us() < 0) {
            return std::nullopt;
        }
        return prev_ + diff;
    }

    // 正常阶段:通过滤波参数计算预估时间
    double timestampDiff = unwrapped_ts90khz - *first_unwrapped_timestamp_;
    TimeDelta diff = TimeDelta::Millis(
        static_cast<int64_t>((timestampDiff - w_[1]) / w_[0] + 0.5) // +0.5用于四舍五入
    );

    // 防止时间为负
    if (start_.us() + diff.us() < 0) {
        return std::nullopt;
    }

    return start_ + diff;
}
4.2、JitterEstimator:抖动延时计算

网络抖动导致帧到达时间不稳定,需通过JitterEstimator计算缓存延时,确保渲染前等待足够时间,避免因帧未到达或重传导致的卡顿。抖动延时由两部分组成:

  1. 传输大帧引起的延迟:大帧传输耗时更长,需额外缓存;
  2. 网络噪声引起的延迟:随机网络波动导致的到达时间偏差,需通过统计模型平滑。

总抖动延时公式:

Jitter Delay = 传输大帧延迟 + 网络噪声延迟
传输大帧延迟 = estimate[0] * (MaxFrameSize - AvgFrameSize)
网络噪声延迟 = kNoiseStdDevs * sqrt(var_noise_ms2_) - kNoiseStdDevOffset

其中:

  • estimate[0]:信道传输速率的倒数(ms/byte);
  • MaxFrameSize/AvgFrameSize:最大/平均帧大小(排除关键帧等异常值);
  • kNoiseStdDevs:噪声系数(固定2.33);
  • var_noise_ms2_:噪声方差;
  • kNoiseStdDevOffset:噪声扣除常数(固定30ms)。

传输大帧延迟计算(GetFrameDelayVariationEstimateSizeBased)

通过简化卡尔曼滤波估算传输速率,进而计算大帧额外延迟。

/**
 * @brief 计算传输大帧引起的额外延迟
 * @param frame_size_variation_bytes 帧大小偏差(当前帧大小 - 平均帧大小)
 * @return double 大帧延迟(单位:ms)
 */
double FrameDelayVariationKalmanFilter::GetFrameDelayVariationEstimateSizeBased(
    double frame_size_variation_bytes) const {
    // 单位换算:[1/bytes per ms] * [bytes] = [ms]
    return estimate_[0] * frame_size_variation_bytes;
}

网络噪声延迟计算(NoiseThreshold)

通过统计噪声方差,计算需要的额外缓存时间以应对随机波动。

/**
 * @brief 计算网络噪声引起的延迟
 * @return double 噪声延迟(单位:ms,最小为1ms)
 */
double JitterEstimator::NoiseThreshold() const {
    // 公式:kNoiseStdDevs * sqrt(噪声方差) - kNoiseStdDevOffset
    double noise_threshold_ms = kNoiseStdDevs * sqrt(var_noise_ms2_) - kNoiseStdDevOffset;
    // 确保延迟不小于1ms(避免负延迟)
    if (noise_threshold_ms < 1.0) {
        noise_threshold_ms = 1.0;
    }
    return noise_threshold_ms;
}

噪声方差计算(EstimateRandomJitter)

通过指数加权移动平均(EWMA)动态更新噪声方差,适应网络波动。

/**
 * @brief 估算网络噪声方差
 * @param d_dT 实际帧延迟 - 预估帧延迟(残差)
 * @param fps 当前视频帧率(用于调整权重)
 */
void JitterEstimator::EstimateRandomJitter(double d_dT, Frequency fps) {
    // 1. 调整权重因子alpha(指数加权,启动阶段逐步收敛)
    alpha_count_++;
    if (alpha_count_ > kAlphaCountMax) {
        alpha_count_ = kAlphaCountMax;
    }
    double alpha = static_cast<double>(alpha_count_ - 1) / static_cast<double>(alpha_count_);

    // 2. 根据帧率缩放alpha(30fps为基准,低帧率需更快适应)
    if (fps > Frequency::Zero()) {
        constexpr Frequency k30Fps = Frequency::Hertz(30);
        double rate_scale = k30Fps / fps;
        // 启动阶段线性插值rate_scale(从1.0过渡到目标值)
        if (alpha_count_ < kFrameProcessingStartupCount) {
            rate_scale = (alpha_count_ * rate_scale + (kFrameProcessingStartupCount - alpha_count_)) 
                        / kFrameProcessingStartupCount;
        }
        alpha = pow(alpha, rate_scale); // 调整alpha权重
    }

    // 3. 指数加权更新平均噪声与噪声方差
    double avg_noise_ms = alpha * avg_noise_ms_ + (1 - alpha) * d_dT;
    double var_noise_ms2 = alpha * var_noise_ms2_ + (1 - alpha) * pow(d_dT - avg_noise_ms, 2);

    // 4. 更新成员变量(确保方差不小于1.0,避免异常值)
    avg_noise_ms_ = avg_noise_ms;
    var_noise_ms2_ = var_noise_ms2 < 1.0 ? 1.0 : var_noise_ms2;
}

重传场景下的RTT延时补充

当检测到帧有重传时,需额外增加RTT(往返时间)延时,等待重传帧到达。

/**
 * @brief 获取最终抖动延时(含RTT补充)
 * @param rtt_multiplier RTT乘数(根据保护模式配置,如NackFEC模式为0.0)
 * @param rtt_mult_add_cap RTT补充延迟上限(可选)
 * @return TimeDelta 最终抖动延时
 */
TimeDelta JitterEstimator::GetJitterEstimate(
    double rtt_multiplier, 
    absl::optional<TimeDelta> rtt_mult_add_cap) {
    Timestamp now = clock_->CurrentTime();
    // 超时重置NACK计数(避免历史重传影响当前计算)
    if (now - latest_nack_ > kNackCountTimeout) {
        nack_count_ = 0;
    }

    // 基础抖动延时(大帧延迟 + 噪声延迟)
    TimeDelta jitter = CalculateEstimate();

    // 重传次数超限:补充RTT延时
    if (nack_count_ >= kNackLimit) {
        TimeDelta rtt_delay = rtt_filter_.Rtt() * rtt_multiplier;
        // 应用RTT补充延迟上限
        if (rtt_mult_add_cap.has_value()) {
            rtt_delay = std::min(rtt_delay, rtt_mult_add_cap.value());
        }
        jitter += rtt_delay;
    }

    // 平滑滤波(避免延时突变)
    if (filter_jitter_estimate_ > jitter) {
        jitter = filter_jitter_estimate_;
    }

    return jitter;
}
4.3、音视频同步(AVSyn)实现

音视频流通过UDP独立传输,且处理路径不同(音频采样率固定,视频帧率动态),易导致渲染时间差与采集时间差不一致。同步的核心思想是:

  • 在接收端对音视频分别设置缓冲延时,确保渲染时间差 = 采集时间差;
  • 通过NTP时间校准发送端与接收端的时间关系,动态调整缓冲延时。

时间差获取(三大关键时间差)

要实现同步,需获取以下三个时间差:

时间差类型获取方式用途
发送端音视频采集时间差通过RTCP SR报文的NTP与RTP时间戳线性关系计算作为同步基准,确保渲染差与采集差一致
接收端音视频接收时间差记录音视频帧的本地接收NTP时间,直接计算差值反映当前网络传输后的时间偏移
音视频渲染缓冲时间差动态调整,使渲染差 = 采集差最终同步控制目标

发送端时间校准(RtpToNtpEstimator)

RTCP SR报文包含发送端的NTP时间与对应RTP时间戳,通过线性回归建立NTP = slope * RTP + offset关系,进而计算任意RTP时间戳对应的采集NTP时间。

(1)参数计算(UpdateParameters)

/**
 * @brief 通过SR报文测量值,计算NTP与RTP的线性回归参数(slope和offset)
 */
void RtpToNtpEstimator::UpdateParameters() {
    size_t n = measurements_.size();
    if (n < 2) { // 至少需要2个测量值才能拟合直线
        return;
    }

    // 线性回归:最小二乘法拟合 y = k*x + b(y=NTP时间,x=解包后的RTP时间戳)
    auto x = [](const RtcpMeasurement& m) { 
        return static_cast<double>(m.unwrapped_rtp_timestamp); 
    };
    auto y = [](const RtcpMeasurement& m) { 
        return static_cast<double>(static_cast<uint64_t>(m.ntp_time)); 
    };

    // 计算x和y的平均值
    double avg_x = 0, avg_y = 0;
    for (const RtcpMeasurement& m : measurements_) {
        avg_x += x(m);
        avg_y += y(m);
    }
    avg_x /= n;
    avg_y /= n;

    // 计算x的方差与x-y的协方差
    double variance_x = 0, covariance_xy = 0;
    for (const RtcpMeasurement& m : measurements_) {
        double normalized_x = x(m) - avg_x;
        double normalized_y = y(m) - avg_y;
        variance_x += normalized_x * normalized_x;
        covariance_xy += normalized_x * normalized_y;
    }

    // 方差过小(测量值几乎重合),无法拟合
    if (std::fabs(variance_x) < 1e-8) {
        return;
    }

    // 计算斜率k(slope)和截距b(offset)
    double k = covariance_xy / variance_x;
    double b = avg_y - k * avg_x;

    // 保存拟合参数
    params_ = {{.slope = k, .offset = b}};
}

(2)采集时间预估(Estimate)

/**
 * @brief 根据RTP时间戳预估发送端采集NTP时间
 * @param rtp_timestamp 音视频帧的RTP时间戳
 * @return NtpTime 预估的采集NTP时间(无效则返回空)
 */
NtpTime RtpToNtpEstimator::Estimate(uint32_t rtp_timestamp) const {
    if (!params_) { // 未拟合参数,无法预估
        return NtpTime();
    }

    // 解包RTP时间戳,代入线性公式计算NTP时间
    double estimated = 
        static_cast<double>(unwrapper_.Unwrap(rtp_timestamp)) * params_->slope 
        + params_->offset + 0.5f; // +0.5f用于四舍五入

    // 转换为NtpTime(截断为uint64_t)
    return NtpTime(rtc::saturated_cast<uint64_t>(estimated));
}

缓冲延时计算(StreamSynchronization)

通过ComputeRelativeDelay计算当前时间偏差,再通过ComputeDelays动态调整音视频的缓冲延时目标。

(1)计算相对延迟(ComputeRelativeDelay)

/**
 * @brief 计算音视频的相对延迟(当前渲染偏差)
 * @param audio_measurement 音频测量数据(含RTP-NTP映射、最新接收时间)
 * @param video_measurement 视频测量数据
 * @param relative_delay_ms 输出:相对延迟(正值表示视频滞后于音频)
 * @return bool 计算成功与否(NTP时间无效则失败)
 */
bool StreamSynchronization::ComputeRelativeDelay(
    const Measurements& audio_measurement,
    const Measurements& video_measurement,
    int* relative_delay_ms) {
    // 1. 预估音视频的发送端采集NTP时间
    NtpTime audio_last_capture_time = 
        audio_measurement.rtp_to_ntp.Estimate(audio_measurement.latest_timestamp);
    NtpTime video_last_capture_time = 
        video_measurement.rtp_to_ntp.Estimate(video_measurement.latest_timestamp);
    if (!audio_last_capture_time.Valid() || !video_last_capture_time.Valid()) {
        return false;
    }

    // 2. 转换为ms单位
    int64_t audio_cap_ms = audio_last_capture_time.ToMs();
    int64_t video_cap_ms = video_last_capture_time.ToMs();

    // 3. 计算相对延迟:(视频接收时间 - 音频接收时间) - (视频采集时间 - 音频采集时间)
    // 正值表示视频接收滞后,需增加视频缓冲或减少音频缓冲
    *relative_delay_ms = 
        (video_measurement.latest_receive_time_ms - audio_measurement.latest_receive_time_ms)
        - (video_cap_ms - audio_cap_ms);

    // 4. 过滤异常值(超出±kMaxDeltaDelayMs则视为无效)
    if (*relative_delay_ms > kMaxDeltaDelayMs || *relative_delay_ms < -kMaxDeltaDelayMs) {
        return false;
    }

    return true;
}

(2)计算目标缓冲延时(ComputeDelays)

/**
 * @brief 计算音视频的目标缓冲延时,实现同步
 * @param relative_delay_ms 相对延迟(当前偏差)
 * @param current_audio_delay_ms 当前音频缓冲延时
 * @param total_audio_delay_target_ms 输出:音频目标缓冲延时
 * @param total_video_delay_target_ms 输出:视频目标缓冲延时
 * @return bool 是否需要调整(偏差在允许范围内则不调整)
 */
bool StreamSynchronization::ComputeDelays(
    int relative_delay_ms,
    int current_audio_delay_ms,
    int* total_audio_delay_target_ms,
    int* total_video_delay_target_ms) {
    int current_video_delay_ms = *total_video_delay_target_ms;

    // 1. 计算当前实际偏差:视频缓冲 - 音频缓冲 + 相对延迟
    int current_diff_ms = current_video_delay_ms - current_audio_delay_ms + relative_delay_ms;

    // 2. 指数加权平滑偏差(避免频繁调整)
    avg_diff_ms_ = ((kFilterLength - 1) * avg_diff_ms_ + current_diff_ms) / kFilterLength;

    // 3. 偏差在允许范围内(±kMinDeltaMs),无需调整
    if (abs(avg_diff_ms_) < kMinDeltaMs) {
        return false;
    }

    // 4. 限制单次调整幅度(避免延时突变)
    int diff_ms = avg_diff_ms_ / 2; // 每次调整一半偏差,防止过冲
    diff_ms = std::min(diff_ms, kMaxChangeMs); // 最大调整幅度上限
    diff_ms = std::max(diff_ms, -kMaxChangeMs); // 最大调整幅度下限
    avg_diff_ms_ = 0; // 调整后重置平均值,避免连续调整

    // 5. 根据偏差方向调整缓冲延时
    if (diff_ms > 0) {
        // 视频滞后:优先减少视频额外缓冲,再增加音频缓冲
        if (video_delay_.extra_ms > base_target_delay_ms_) {
            video_delay_.extra_ms -= diff_ms;
            audio_delay_.extra_ms = base_target_delay_ms_;
        } else {
            audio_delay_.extra_ms += diff_ms;
            video_delay_.extra_ms = base_target_delay_ms_;
        }
    } else {
        // 音频滞后:优先减少音频额外缓冲,再增加视频缓冲
        if (audio_delay_.extra_ms > base_target_delay_ms_) {
            audio_delay_.extra_ms += diff_ms; // diff_ms为负,即减少
            video_delay_.extra_ms = base_target_delay_ms_;
        } else {
            video_delay_.extra_ms -= diff_ms; // diff_ms为负,即增加
            audio_delay_.extra_ms = base_target_delay_ms_;
        }
    }

    // 6. 确保缓冲延时不低于基准值,不高于最大值
    video_delay_.extra_ms = std::max(video_delay_.extra_ms, base_target_delay_ms_);
    int new_video_delay_ms = std::min(video_delay_.extra_ms, 
                                     base_target_delay_ms_ + kMaxDeltaDelayMs);
    int new_audio_delay_ms = std::min(audio_delay_.extra_ms, 
                                     base_target_delay_ms_ + kMaxDeltaDelayMs);

    // 7. 更新目标延时
    video_delay_.last_ms = new_video_delay_ms;
    audio_delay_.last_ms = new_audio_delay_ms;
    *total_video_delay_target_ms = new_video_delay_ms;
    *total_audio_delay_target_ms = new_audio_delay_ms;

    return true;
}

延时参数生效(UpdateDelay)

通过RtpStreamsSynchronizer::UpdateDelay将计算出的目标延时应用到音视频流的播放参数中。

/**
 * @brief 生效音视频目标缓冲延时,完成同步配置
 * @param audio_info 音频流信息(含当前延时)
 * @param video_info 视频流信息(含当前延时)
 * @param log_stats 是否打印日志
 */
void RtpStreamsSynchronizer::UpdateDelay(
    const StreamInfo& audio_info,
    const StreamInfo& video_info,
    bool log_stats) {
    int relative_delay_ms = 0;
    // 1. 计算相对延迟
    if (!sync_->ComputeRelativeDelay(audio_info.measurements, 
                                     video_info.measurements, 
                                     &relative_delay_ms)) {
        return;
    }

    int target_audio_delay_ms = 0;
    int target_video_delay_ms = video_info.current_delay_ms;
    // 2. 计算目标延时
    if (!sync_->ComputeDelays(relative_delay_ms, 
                              audio_info.current_delay_ms, 
                              &target_audio_delay_ms, 
                              &target_video_delay_ms)) {
        return;
    }

    // 3. 打印同步日志(可选)
    if (log_stats) {
        int64_t now_ms = rtc::TimeMillis();
        RTC_LOG(LS_INFO) << "Sync delay stats: " << now_ms
                        << ", {ssrc:" << sync_->audio_stream_id() 
                        << ", target_delay_ms:" << target_audio_delay_ms << "}"
                        << ", {ssrc:" << sync_->video_stream_id() 
                        << ", target_delay_ms:" << target_video_delay_ms << "}";
    }

    // 4. 应用音频目标延时
    if (!syncable_audio_->SetMinimumPlayoutDelay(target_audio_delay_ms)) {
        sync_->ReduceAudioDelay(); // 应用失败时降低音频延时
    }

    // 5. 应用视频目标延时(最终调用VCMTiming::set_max_playout_delay)
    if (!syncable_video_->SetMinimumPlayoutDelay(target_video_delay_ms)) {
        sync_->ReduceVideoDelay(); // 应用失败时降低视频延时
    }
}

WebRTC通过TimestampExtrapolator、JitterEstimator与音视频同步模块的协同,实现了视频渲染平滑与音视频同步的QoS保障:

  1. 渲染时间预估:基于卡尔曼滤波的TimestampExtrapolator,精准预测帧接收时间,为渲染时序提供基础;
  2. 抖动延时控制:JitterEstimator通过大帧延迟与噪声延迟的计算,确保缓存足够时间应对网络波动与重传;
  3. 音视频同步:通过RTCP SR报文校准发送端时间,动态调整音视频缓冲延时,确保渲染时间差 = 采集时间差。

三大模块的结合,有效解决了实时音视频通信中的卡顿、不同步问题。

Logo

邀请您加入社区

更多推荐