WebRTC 之音视频同步

在网络视频会议中, 我们常会遇到音视频不同步的问题, 我们有一个专有名词 lip-sync 唇同步来描述这类问题,当我们看到人的嘴唇动作与听到的声音对不上的时候,不同步的问题就出现了

而在线会议中, 听见清晰的声音是优先级最高的, 人耳对于声音的延迟是很敏感的

根据 T-REC-G.114-200305 中的描述

  • 大于~280ms 有些用户就会不满意
  • 大于~380ms 多数用户就会不满意
  • 大于~500ms 几乎所有用户就会不满意

我们就尽量使得声音的延迟在 280 ms 之内,这是解决 lip-sync 问题的前提, 声音不好的严重程序超过音视频不同步。

我们可以定义一个 sync_diff 值 来表示音频帧和视频帧之间的时间差

  • 正值表示音频领先于视频
  • 负值表示音频落后于视频

ITU 对此给出以下的阈值:

  • 不可感知 Undetectability (-100ms, +25ms)
  • 可感知 Detectability: (-125ms, +45ms)
  • 可接受 Acceptability: (--185ms, +90 ms)
  • 影响用户 Impact user experience (-∞, -185ms) ∪ (+90ms,∞)

(ITU-R BT.1359-1, Relative Timing of Sound and Vision for Broadcasting" 1998. Retrieved 30 May 2015)

当我们在播放一个视频帧及对应的音频帧的时候,要计算一下这个 sync_diff

sync_diff = audio_frame_time - video_frame_time

如果这个 sync_diff 大于 90ms, 也就是音频包到得过早,就会有音视频不同步的问题 - 声音听到了,嘴巴没跟上.

如果这个 sync_diff 小于 -185ms, 也就是视频包到得过早,就会有音视频不同步的问题 - 嘴巴在动,声音没跟上.

不同步的原因


lip sync 1

这个问题的原因主要在于音频的采集, 编码,传输, 解码, 播放与视频的采集,编码,传输,解码以及渲染一般是分开进行的,因为音频和视频采集自不同的设备,即它们的来源不同,在网络上传输也会有延迟,也由不同的设备进行播放,这样如果在接收方不采取措施进行时间同步,就会极有可能看到口型和听到的声音对不上的情况。

由此派生出 3 个小问题:

  1. 如何将来自同一个人或设备的多路 audio 及 video stream关联起来?
  2. 如何将 RTP 中的时间戳 timestamp 映射到发送方的音视频采集时间
  3. 如何调整音频或者视频帧的播放时间,让它们怎么之间相对同步?

解决方案

1. 如何将来自同一个人或设备的音视频流关联起来?

对于多媒体会话,每种类型的媒体(例如音频或视频)一般会在单独的 RTP 会话中发送,发送方会在 RTCP SDES 消息中指明

接收方通过 CNAME 项关联要同步的RTP流, 而这个 CNAME 包含在发送方所发送的 RTCP SDES 中

SDES 数据包包含常规包头,有效负载类型为 202,项目计数等于数据包中 SSRC/CSRC 块的数量,后跟零个或多个 SSRC/CSRC 块,其中包含有关特定 SSRC 或 CSRC,每个都与 32 位边界对齐。

0               1               2               3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |V=2|P|    SC   |  PT=SDES=202  |            length L           |
    +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
    |                          SSRC/CSRC_1                          |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                           SDES items                          |
    |                              ...                              |
    +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
    |                          SSRC/CSRC_2                          |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                           SDES items                          |
    |                              ...                              |
    +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+

CNAME 项在每个 SDES 数据包中都是必需的,而 SDES 数据包又是每个复合 RTCP 数据包中的必需部分。

与 SSRC 标识符一样,CNAME 必须与其他会话参与者的 CNAME 不同。 但 CNAME 不应随机选择 CNAME 标识符,而应允许个人或程序通过 CNAME 内容来定位其来源。

0               1               2               3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |    CNAME=1    |     length    | user and domain name         ...
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

例如 Alice 向外发送一路音频流,一路视频流, 这两路流会使用不同的 SSRC, 但是在其所发送的 RTCP SDES 消息会使用相同的 CNAME.

  • RTP SSRC 1 ~ CNAME 1
  • RTP SSRC 2 ~ CNAME 1

2. 同步的时间如何计算

来自同一个终端用户的音频和视频, 在编码发送的 RTP 包中有一个 timestamp, 这个时间戳表示媒体流的捕捉时间。

同时, 作为发送者也会发送 RTCP Sender Report, 其中包含发送的 RTP timestamp 和 NTP timestamp 的映射关系,这样我们在接收方就可以把 RTP 包里的

lip sync flow

对于每个 RTP 流,发送方定期发出 RTCP SR, 其中包含一对时间戳:

NTP 时间戳以及与该 RTP 流关联的相应 RTP 时间戳。

这对时间戳传达每个媒体流的 NTP 时间和 RTP 时间之间的关系。

先回顾一下 RTP packet 和 RTCP sender report

  • RTP 包结构

    0 1 2 3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |V=2|P|X| CC |M| PT | sequence number |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | timestamp |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | synchronization source (SSRC) identifier |
    +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
    | contributing source (CSRC) identifiers |
    | .... |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

  • RTCP Sender Report 结构

    0 1 2 3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    header |V=2|P| RC | PT=SR=200 | length |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | SSRC of sender |
    +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
    sender | NTP timestamp, most significant word |
    info +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | NTP timestamp, least significant word |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | RTP timestamp |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | sender's packet count |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | sender's octet count |
    +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
    report | SSRC_1 (SSRC of first source) |
    block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    1 | fraction lost | cumulative number of packets lost |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | extended highest sequence number received |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | interarrival jitter |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | last SR (LSR) |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | delay since last SR (DLSR) |
    +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
    report | SSRC_2 (SSRC of second source) |
    block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    2 : ... :
    +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
    | profile-specific extensions |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

通过 NTP timestamp 和 RTP timestamp 之间的映射, 我们可以知道 audio 包的时间和 video 包的时间。

具体的计算可以参见 WebRTC 的 RtpToNtpEstimator 类, 它将收到的若干 SR 中的 NTP time 和 RTP timestamp 保存下来,然后 应用最小二乘法来估算后续 RTP timestamp 所对应的 NTP timestamp, 大致为用最近 N=20 个 RTCP SR 包的 ntp timestamp 和 rtp timestamp 的构造出线性关系 y = ax + b, 通过最小二乘法来计算收到的 RTP 包对应的 ntp timestamp.

// Converts an RTP timestamp to the NTP domain.
// The class needs to be trained with (at least 2) RTP/NTP timestamp pairs from
// RTCP sender reports before the convertion can be done.
class RtpToNtpEstimator {
      public:
            //...

            enum UpdateResult { kInvalidMeasurement, kSameMeasurement, kNewMeasurement };
            // Updates measurements with RTP/NTP timestamp pair from a RTCP sender report.
            UpdateResult UpdateMeasurements(NtpTime ntp, uint32_t rtp_timestamp);

            // Converts an RTP timestamp to the NTP domain.
            // Returns invalid NtpTime (i.e. NtpTime(0)) on failure.
            NtpTime Estimate(uint32_t rtp_timestamp) const;

            // Returns estimated rtp_timestamp frequency, or 0 on failure.
            double EstimatedFrequencyKhz() const;

      private:
            // Estimated parameters from RTP and NTP timestamp pairs in `measurements_`.
            // Defines linear estimation: NtpTime (in units of 1s/2^32) =
            //   `Parameters::slope` * rtp_timestamp + `Parameters::offset`.
            struct Parameters {
                  double slope;
                  double offset;
            };

            // RTP and NTP timestamp pair from a RTCP SR report.
            struct RtcpMeasurement {
                  NtpTime ntp_time;
                  int64_t unwrapped_rtp_timestamp;
            };

            void UpdateParameters();

            int consecutive_invalid_samples_ = 0;
            std::list<RtcpMeasurement> measurements_;
            absl::optional<Parameters> params_;
            mutable RtpTimestampUnwrapper unwrapper_;
};

3. 调整播放和渲染时间

一般我们会以 audio 为主, video 向 audio 靠拢, 两者时间一致也就会达到 lip sync 音视频同步

  1. audio 包先来, video 包后来: audio 包放在 jitter buffer 时等一会儿, 但是这个时间是有限的, 音频的流畅是首先要保证的, 视频跟不上可以降低视频的码率
  2. video 包先来, audio 包后来: video 包要等 audio 包来, 这是为了让音视频同步要付出的代价

一般以音频为主流 master stream,视频为从流 slave stream。 一般方法是接收方维护音频流的缓冲区的管理,并通过将视频 RTP 时间戳转换为正确从属于音频流的时间戳来调整视频流的播放。

当带有RTP时间戳 RTPv的视频帧到达接收器时,接收器通过四个步骤将RTP时间戳 RTPv 映射到视频设备时间戳VTB( Video Time Base),如图所示。

  1. 使用 Video RTCP SR 中的 RTP/NTP 时间戳对建立的映射,将视频 RTP 时间戳 RTPv 映射到发送方 NTP 时间。

  2. 根据该 NTP 时间戳,使用 Audio RTCP SR 中的 RTP/NTP 时间戳对建立的映射,计算来自发送方的相应音频 RTPa 时间戳。

    此时,视频RTP时间戳被映射到音频RTP 包的相同时间基准。

  3. 根据该音频 RTP 时间戳,使用卡尔曼滤波的方法计算音频设备时间基准中的相应时间戳。 结果是音频设备时间基准 ATB(Audio Time Base) 中的时间戳。

  4. 根据 ATB,使用偏移量 AtoV 计算视频设备时基 VTB 中的相应时间戳。

接收方需要确保带有 RTP 时间戳 RTPv 的视频帧使用所计算出的发送方视频设备时间基准 VTB 播放。

AtoV = V_time - A_Time/(audio sample rate)

注:

  • AtoV: 音频相较视频的偏移量
  • ATB: Audio device Time Base 音频设备的时间基准
  • VTB: Video device Time Base 视频设备的时间基准

具体方法可以参见 https://www.ccexpert.us/video-conferencing/using-rtcp-for-media-synchronization.html)

WebRTC 的做法原理上差不多,实现略有不同,可以参见 WebRTC 的源代码 StreamSynchronization 类和 RtpStreamsSynchronizer 类

大致上它会计算出 video 的延迟

current_delay_ms = max(min_playout_delay_ms, jitter_delay_ms + decode_time _ms + render_delay_ms)

然后再计算视频相对于音频的延迟 relative_delay_ms,

  • 如果它大于0, 视频比音频慢,减小视频延迟(主要是调整 jitter buffer delay),或者是增大音频延迟, 取决于阈值 base_target_delay_ms
  • 如果它小于0, 音频比视频慢,减小音频延迟,或者是增大视频延迟, 取决于阈值base_target_delay_ms

base_target_delay_ms 的比较逻辑参见StreamSynchronization::ComputeDelays,

if (diff_ms > 0) {
      // The minimum video delay is longer than the current audio delay.
      // We need to decrease extra video delay, or add extra audio delay.
      if (video_delay_.extra_ms > base_target_delay_ms_) {
            // We have extra delay added to ViE. Reduce this delay before adding
            // extra delay to VoE.
            video_delay_.extra_ms -= diff_ms;
            audio_delay_.extra_ms = base_target_delay_ms_;
      } else {  // video_delay_.extra_ms > 0
            // We have no extra video delay to remove, increase the audio delay.
            audio_delay_.extra_ms += diff_ms;
            video_delay_.extra_ms = base_target_delay_ms_;
      }
      } else {  // if (diff_ms > 0)
      // The video delay is lower than the current audio delay.
      // We need to decrease extra audio delay, or add extra video delay.
      if (audio_delay_.extra_ms > base_target_delay_ms_) {
            // We have extra delay in VoiceEngine.
            // Start with decreasing the voice delay.
            // Note: diff_ms is negative; add the negative difference.
            audio_delay_.extra_ms += diff_ms;
            video_delay_.extra_ms = base_target_delay_ms_;
      } else {  // audio_delay_.extra_ms > base_target_delay_ms_
            // We have no extra delay in VoiceEngine, increase the video delay.
            // Note: diff_ms is negative; subtract the negative difference.
            video_delay_.extra_ms -= diff_ms;  // X - (-Y) = X + Y.
            audio_delay_.extra_ms = base_target_delay_ms_;
      }
}

更多细节在 WebRTC 的代码中

  • class StreamSynchronization
  • class RtpStreamsSynchronizer

通过StreamSynchronization::ComputeDelays计算出音频和视频的相对延迟,如果相对延迟很小( < 30ms), 则无需调整音视频的播放时间,如果相对延迟很大, 则以 80ms 的幅度进行逐步调整。 与传统的只调视频延迟,不调音频延迟, WebRTC 会两边都调点,使得音视频的时间彼此靠近,前提是音频的延迟是在上面提到的可接受范围之内。

参考资料

相关推荐
hunandede37 分钟前
FFmpeg 4.3 音视频-多路H265监控录放C++开发十三.2:avpacket中包含多个 NALU如何解析头部分析
c++·ffmpeg·音视频
声网42 分钟前
「人眼视觉不再是视频消费的唯一形式」丨智能编解码和 AI 视频生成专场回顾@RTE2024
人工智能·音视频
Mr.简锋4 小时前
opencv视频读写
人工智能·opencv·音视频
春末的南方城市5 小时前
开源音乐分离器Audio Decomposition:可实现盲源音频分离,无需外部乐器分离库,从头开始制作。将音乐转换为五线谱的程序
人工智能·计算机视觉·aigc·音视频
Hali_Botebie6 小时前
采样率22050,那么CHUNK_SIZE 一次传输的音频数据大小设置多少合适?unity接收后出现卡顿的问题的思路
音视频
风之馨技术录6 小时前
智谱AI清影升级:引领AI视频进入音效新时代
人工智能·音视频
晚点吧7 小时前
视频横屏转竖屏播放-使用人脸识别+目标跟踪实现
人工智能·目标跟踪·音视频
EasyCVR7 小时前
ISUP协议视频平台EasyCVR视频设备轨迹回放平台智慧农业视频远程监控管理方案
服务器·网络·数据库·音视频
cuijiecheng20189 小时前
音视频入门基础:MPEG2-TS专题(3)——TS Header简介
音视频
学编程的小程16 小时前
【安全通信】告别信息泄露:搭建你的开源视频聊天系统briefing
安全·开源·音视频